diff --git a/product/ERP5/Document/TextDocument.py b/product/ERP5/Document/TextDocument.py
index 7a96f257ad2b6979a43c1d6c556484fe7016d265..f3a97fbc55e3e789fbfed255d072bfbf9cf315fb 100644
--- a/product/ERP5/Document/TextDocument.py
+++ b/product/ERP5/Document/TextDocument.py
@@ -26,6 +26,7 @@
 #
 ##############################################################################
 
+from AccessControl.ZopeGuards import guarded_getattr
 from AccessControl import ClassSecurityInfo
 from zLOG import LOG, WARNING
 from Products.ERP5Type.Base import WorkflowMethod
@@ -37,6 +38,11 @@ from Products.ERP5Type.WebDAVSupport import TextContent
 from Products.CMFDefault.utils import isHTMLSafe
 import re
 
+try:
+  from string import Template
+except ImportError:
+  from Products.ERP5Type.patches.string import Template
+
 DEFAULT_TEXT_FORMAT = 'text/html'
 
 class TextDocument(Document, TextContent):
@@ -162,6 +168,26 @@ class TextDocument(Document, TextContent):
       # check if document has set text_content and convert if necessary
       text_content = self.getTextContent()
       if text_content is not None:
+        # If a method for string substitutions of the text content, perform it.
+        # Decode everything into unicode before the substitutions, in order to
+        # avoid encoding errors.
+        method_id = self.getTextContentSubstitutionMappingMethodId()
+        if method_id:
+          mapping = guarded_getattr(self, method_id)()
+
+          if isinstance(text_content, str):
+            text_content = text_content.decode('utf-8')
+
+          unicode_mapping = {}
+          for k, v in mapping.iteritems():
+            if isinstance(v, str):
+              v = v.decode('utf-8')
+            elif not isinstance(v, unicode):
+              v = str(v).decode('utf-8')
+            unicode_mapping[k] = v
+
+          text_content = Template(text_content).substitute(unicode_mapping)
+
         portal_transforms = getToolByName(self, 'portal_transforms')
         result = portal_transforms.convertToData(mime_type, text_content,
                                                  object=self, context=self,
diff --git a/product/ERP5/PropertySheet/TextDocument.py b/product/ERP5/PropertySheet/TextDocument.py
index 201f16ba54366ae92bd62749b5366d9ba57eb495..8bb4bc68f40f25fae142c9be7cd274877b92cadf 100644
--- a/product/ERP5/PropertySheet/TextDocument.py
+++ b/product/ERP5/PropertySheet/TextDocument.py
@@ -37,6 +37,11 @@ class TextDocument:
             'type'        : 'text',
             'mode'        : 'w'
             },
+        {   'id'          : 'text_content_substitution_mapping_method_id',
+            'description' : 'The method ID which returns a mapping for substitutions of a text content',
+            'type'        : 'string',
+            'mode'        : 'w'
+            },
         {   'id'          : 'text_format',
             'description' : 'The format of the text content of this document',
             'type'        : 'string',
diff --git a/product/ERP5/tests/testERP5Web.py b/product/ERP5/tests/testERP5Web.py
index b6a370f4baef0b579738f9d5a59acdb384896aea..3db9963f2bd3cade4fd54e56c4854a629e0f6a31 100644
--- a/product/ERP5/tests/testERP5Web.py
+++ b/product/ERP5/tests/testERP5Web.py
@@ -362,6 +362,44 @@ class TestERP5Web(ERP5TypeTestCase, ZopeTestCase.Functional):
     self.logout()
     self.assertRaises(Unauthorized,  websection._getExtensibleContent,  request,  document_reference)
     
+  def test_07_WebPageTextContentSubstituions(self, quiet=quiet, run=run_all_test):
+    """
+      Simple Case of showing the proper text content with and without a substitution
+      mapping method.
+    """
+    if not run:
+      return
+    if not quiet:
+      message = '\ntest_07_WebPageTextContentSubstituions'
+      ZopeTestCase._print(message)
+
+    content = '<a href="${toto}">$titi</a>'
+    substituted_content = '<a href="foo">bar</a>'
+    mapping = dict(toto='foo', titi='bar')
+
+    portal = self.getPortal()
+    document = portal.web_page_module.newContent(portal_type='Web Page', 
+            text_content=content)
+   
+    # No substitution should occur.
+    self.assertEquals(document.asStrippedHTML(), content)
+
+    klass = document.__class__
+    klass.getTestSubstitutionMapping = lambda self, **kw: mapping
+    document.setTextContentSubstitutionMappingMethodId('getTestSubstitutionMapping')
+
+    # Substitutions should occur.
+    # XXX purge transformation cache.
+    if hasattr(document, '_v_transform_cache'):
+      delattr(document, '_v_transform_cache')
+    self.assertEquals(document.asStrippedHTML(), substituted_content)
+
+    klass._getTestSubstitutionMapping = klass.getTestSubstitutionMapping
+    document.setTextContentSubstitutionMappingMethodId('_getTestSubstitutionMapping')
+
+    # Even with the same callable object, a restricted method id should not be callable.
+    self.assertRaises(Unauthorized, document.asStrippedHTML)
+
 def test_suite():
   suite = unittest.TestSuite()
   suite.addTest(unittest.makeSuite(TestERP5Web))