6

I'm copying components between versions of essentially the same schema, of which the latter has a stricter XSLT attached to it's RTF field.

The code (TOM.Net):

value = ((TextField)oldField).Value;

var def = (XhtmlFieldDefinition)field.Definition;
var tfm = new XslCompiledTransform();
tfm.Load(def.FilterXslt);

var settings = new XmlReaderSettings();
settings.ConformanceLevel = ConformanceLevel.Fragment;

using (var sw = new StringWriter())
{
    using (var sr = new StringReader(value))
    {
        using (var reader = XmlReader.Create(sr, settings))
        {
            tfm.Transform(reader, null, sw);
            field.Value = sw.ToString();
        }
    }
}

The problems i'm getting:

  1. If it runs "as is", all XML is stripped off. That's likely because input value has xmlns="http://www.w3.org/1999/xhtml" declaration on some (not all, by the way) of it's HTML tags, but in XSLT these tags are not bound to anything but default xmlns="http://www.w3.org/1999/XSL/Transform".

  2. If i do an unfair move by exterminating "xmlns" in content, say, like this: new StringReader(value.Replace(@" xmlns=""http://www.w3.org/1999/xhtml""", string.Empty)), then transform works fine, but upon calling component.Save(), it says something like this:

XML validation error. Reason: The element 'text' in namespace 'http://www.unilever.com/general' has invalid child element 'h2'. List of possible elements expected: any element in namespace 'http://www.w3.org/1999/xhtml'..

As i understand, the proper way of tackling my problem would be to alter XSLT in such a way as to: 1. Declare XHTML namespace, 2. Modify XPath expressions to use FQ names.

Unfortunately, modifying field's XSLT is not an option.

Also, ILSpy cannot find any references of XhtmlFieldDefinition.FilterXslt to get an idea how Tridion itself does it.

How to transform XHtmlField value according to attached filter XSLT?

EDIT LinqPad snipped that demonstrates the transformation (but not the behavior during Save!): https://gist.github.com/esteewhy/6248993

EDIT (Solution) Got it working using user978511 idea/hack of re-applying XHTML namespace to top-most elements. Still would be curious how Tridion does it internally. Nevertheless, solutions looks like this:

// remove XHTML NS which confused XSLT
var input = value.Replace(@"xmlns=""http://www.w3.org/1999/xhtml""", string.Empty);

// apply XSLT as shown above

// put back XHTML NS on top-most elements
field.Value = AssignTopLevelNS(sb.ToString());

// .. and the essential method itself
static string AssignTopLevelNS(string xmlFragment)
{
    var k = new XmlDocument();
    k.LoadXml("<xml>" + xmlFragment + "</xml>");
    var xKey = k.CreateAttribute("xmlns");
    xKey.Value = "http://www.w3.org/1999/xhtml";

    k.DocumentElement.ChildNodes.Cast<XmlNode>().ToList().ForEach(element => {
        element.Attributes.SetNamedItem(xKey);
    });

    return k.DocumentElement.InnerXml;
}
2
  • Can you post your component and XSLT? Commented Aug 19, 2013 at 7:56
  • yes, here please: gist.github.com/esteewhy/… (the output of "Source" tab, not sure if TOM.Net output w'd differ)
    – esteewhy
    Commented Aug 19, 2013 at 8:05

3 Answers 3

4
+50

So why don't you put the namespace you've deleted back?

        // provided b is the output of your transformation
        var k = new XmlDocument();
        k.LoadXml("<txt>" + b + "</txt>");
        var t = k.DocumentElement.ChildNodes;


        XmlAttribute xKey = k.CreateAttribute("xmlns");
        xKey.Value = "http://www.w3.org/1999/xhtml";
        foreach (XmlNode element in t)
        {
            element.Attributes.SetNamedItem(xKey);
        }

        Console.WriteLine(k.DocumentElement.InnerXml);

Disregard the deep meaning of variable names :)

This will put the namespace back, but only on "top" elements, like in your original XML.

But all in all this is just an ugly hack, you should realy change your XSLT. BTW, did you notice that <img> tag is stripped out by XSLT?

1
  • Indeed it worked! That is: 1. Stripping off XHTML NS; 2. Applying XSLT; 3. Assigning back XHTML NS to top-most elements <= brilliant idea: you don't really need to care about the whole hierarchy!
    – esteewhy
    Commented Aug 22, 2013 at 15:04
5

the reason this wont save is because the namespace is missing from the richtext node, if you open the source of a component you`ll see that the component is in a namespace, meaning that anything within the richtext will need the correct namespace in order to save, below is a working example of creating a component with richtext html

also uses the fields library~: http://code.google.com/p/tridion-practice/wiki/ChangeContentOrMetadata

public static string filterXhtmlWithXslt(string input)
    {
        input = "<body xmlns=\"http://www.w3.org/1999/xhtml\">" + input + "</body>";

        var xd = new XmlDocument();
        xd.LoadXml(input);

        var xslt = new System.Xml.Xsl.XslCompiledTransform();
        xslt.Load("xslt/Filtering.xslt");

        var transformedDoc = new XmlDocument();

        using (var xw = transformedDoc.CreateNavigator().AppendChild())
        {
            xslt.Transform(xd, null, xw);
        }

        return transformedDoc.FirstChild.InnerXml;
    }

xslt/Filtering.xslt

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      xmlns:xhtml="http://www.w3.org/1999/xhtml" version="1.0">
      <xsl:output omit-xml-declaration="yes" method="xml" cdata-section-elements="script"></xsl:output>
      <xsl:template match="/">
        <xsl:apply-templates select="*"></xsl:apply-templates>
      </xsl:template>
      <xsl:template match="xhtml:body|xhtml:p|xhtml:br|xhtml:ol|xhtml:ul|xhtml:li|xhtml:sup|xhtml:sub|xhtml:a|xhtml:table|xhtml:tr|xhtml:td|xhtml:th|xhtml:hr|xhtml:h2|xhtml:h3|xhtml:h4|xhtml:h5|xhtml:h6">
        <xsl:copy>
          <xsl:apply-templates select="node()|@xhtml:href|@xhtml:alt|@xhtml:title|@xhtml:src|@xhtml:target"></xsl:apply-templates>
        </xsl:copy>
      </xsl:template>
      <xsl:template match="@*">
        <xsl:copy>
          <xsl:value-of select="."></xsl:value-of>
        </xsl:copy>
      </xsl:template>
      <xsl:template match="*[      (self::xhtml:br or self::xhtml:p or self::xhtml:div)     and      normalize-space(translate(., &apos; &apos;, &apos;&apos;)) = &apos;&apos;     and      not(@*)     and      not(processing-instruction())     and      not(comment())     and      not(*[not(self::xhtml:br) or @* or * or node()])     and      not(following::node()[not(         (self::text() or self::xhtml:br or self::xhtml:p or self::xhtml:div)        and         normalize-space(translate(., &apos; &apos;, &apos;&apos;)) = &apos;&apos;        and         not(@*)        and         not(processing-instruction())        and         not(comment())        and         not(*[not(self::xhtml:br) or @* or * or node()])       )])     ]">
        <!-- ignore all paragraphs and line-breaks at the end that have nothing but (non-breaking) spaces and line breaks -->
      </xsl:template>
      <xsl:template match="xhtml:br[parent::xhtml:div and not(preceding-sibling::node()) and not(following-sibling::node())]">
        <!-- Chrome generates <div><br/></div>. Renders differently in different browsers. Replace it with a non-breaking space -->
        <xsl:text> </xsl:text>
      </xsl:template>
    </xsl:stylesheet>

then you can call the function like so

    private void Create_button(object sender, RoutedEventArgs e)
    {


        ICoreService client = Functions.GetNewClient("http://TRIDION_URL/webservices/CoreService2012.svc/basicHttp", "administrator", "PASSWORD_GOES_HERE");
        ReadOptions DEFAULT_READ_OPTIONS = new ReadOptions { LoadFlags = LoadFlags.Expanded };

        string schemaUri = "tcm:7-1270-8";
        string folderUri = "tcm:7-1031-2";

        var schemaFields = client.ReadSchemaFields(schemaUri, true, DEFAULT_READ_OPTIONS);
        var component = client.GetDefaultData(ItemType.Component, folderUri, DEFAULT_READ_OPTIONS) as ComponentData;
        //Console.WriteLine(component.Content);
        System.Diagnostics.Debug.WriteLine(component.Content);

        var fields = Fields.ForContentOf(schemaFields);

        component.Title = "Name of component (created at " + DateTime.Now + ")";
        fields["Title"].Value = "Title of newly created component";
        fields["RichText"].Value = Functions.filterXhtmlWithXslt(@"     <p xmlns=""http://www.w3.org/1999/xhtml"">This is <span style=""font-family: &apos;Courier New&apos;; font-size: 14pt;"">my</span> rich <strong>text</strong> test</p>


    <p xmlns=""http://www.w3.org/1999/xhtml"">
        <br></br>
    </p>


    <p xmlns=""http://www.w3.org/1999/xhtml"">And a</p>


    <h2 xmlns=""http://www.w3.org/1999/xhtml"">HEADING</h2>


    <h1 xmlns=""http://www.w3.org/1999/xhtml"">wooo</h1>mm");

        component.Content = fields.ToString();
        Console.WriteLine(component.Content);
        component.Schema.IdRef = schemaUri;
        client.Create(component, DEFAULT_READ_OPTIONS);
        Output(component.Id);
        Output(component.Content);
    }
2
  • Hi Mark. I'm sure these code snippets are useful, but just reading the question, I'm not sure how your answer answers it. Maybe if you put some explanatory text in too, you'll get a few up-votes. Commented Aug 19, 2013 at 20:34
  • modified my response, I hope this is more helpful
    – Mark
    Commented Aug 21, 2013 at 12:10
4

I'd be interested to know the use-case around this, i've certainly never heard of this sort of requirement before.

Not the best solution, but it would work.....

Is it possible that you know all the validation changes in the RTF field?, you can simply select them for example all elements of 'h2' and replace them with the supported tag (or remove) required by the content?

I'd likely built a list of the tags that are now supposed, a mapping for which items to convert to the supported tags, which potential ones are thrown away and process each relevant component against this rule.

3
  • John, the use-case is content porting from 2009 to 2013. CP is used to transfer objects mostly intact to new system. Then custom-built tool is supposed to transform from "old" schemas to "new", and this is where RTF filtering pops up. The deal is: it (FilterXslt approach) should work, but it doesn't. Custom filtering is certainly possible (as anything else given adequate effort:-|)
    – esteewhy
    Commented Aug 20, 2013 at 7:37
  • 2
    why not copy the 2009 db, then run the db upgrade script - plug that into a CME and content export from it? this means your exported content is in 2011 format and ready to go?
    – johnwinter
    Commented Aug 21, 2013 at 19:32
  • this would seem like a better option
    – Mark
    Commented Mar 16, 2017 at 18:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.