public static string GetHtmlForEntry(XmlNode entry)
        {
            var b = new StringBuilder();

            b.AppendLine("<div class='entry'><table>");

            var lexicalUnitNode = entry.SelectSingleNode("lexical-unit");
            if (lexicalUnitNode != null)
            {
                AddMultiTextHtml(b,  "lexeme form", lexicalUnitNode);
            }
            foreach (XmlNode node in entry.SafeSelectNodes("citation"))
            {
                AddMultiTextHtml(b,  "citation form", node);
            }
            foreach (XmlNode field in entry.SafeSelectNodes("relation"))
            {
                var type = field.GetStringAttribute("type");
                var id = field.GetStringAttribute("ref");

                var formNode = GetFormNodeForReferencedEntry(entry.OwnerDocument, id);
                if (null==formNode)
                {
                    b.AppendFormat("Could not locate {0}", id);
                    continue;
                }
                AddMultiTextHtml(b, type, formNode);
            }
            foreach (XmlNode field in entry.SafeSelectNodes("field"))
            {
                var label = field.GetStringAttribute("type");
                AddMultiTextHtml(b,  label, field);
            }
            foreach (XmlNode note in entry.SafeSelectNodes("note"))
            {
                AddMultiTextHtml(b, "note", note);
            }
            foreach (XmlNode node in entry.SafeSelectNodes("sense"))
            {
                AddSense(b, 0, node);
            }
            b.AppendLine("</table></div>");
            return b.ToString();
        }
 private static void DoPostMerge(string outputPath, XmlNode mergedNode)
 {
     foreach (XmlNode partNode in mergedNode.SafeSelectNodes("layout/generate"))
     {
         partNode.Attributes.Remove(partNode.Attributes["combinedkey"]);
     }
     using (var writer = XmlWriter.Create(outputPath, CanonicalXmlSettings.CreateXmlWriterSettings()))
     {
         writer.WriteNode(mergedNode.CreateNavigator(), true);
     }
 }
 /// <summary>
 /// Normally, the connection between bloom-translationGroups and the dataDiv is that each bloom-editable child
 /// (which has an @lang) pulls the corresponding string from the dataDiv. This happens in BookData.
 ///
 /// That works except in the case of xmatter which a) start empty and b) only normally get filled with
 /// .bloom-editable's for the current languages. Then, when bloom would normally show a source bubble listing
 /// the string in other languages, well there's nothing to show (the bubble can't pull from dataDiv).
 /// So our solution here is to pre-pack the translationGroup with bloom-editable's for each of the languages
 /// in the data-div.
 /// The original (an possibly only) instance of this is with book titles. See bl-1210.
 /// </summary>
 public static void PrepareDataBookTranslationGroups(XmlNode pageOrDocumentNode, IEnumerable<string> languageCodes)
 {
     //At first, I set out to select all translationGroups that have child .bloomEditables that have data-book attributes
     //however this has implications on other fields, noticeably the acknowledgments. So in order to get this fixed
     //and not open another can of worms, I've reduce the scope of this
     //fix to just the bookTitle, so I'm going with findOnlyBookTitleFields for now
     var findAllDataBookFields = "descendant-or-self::*[contains(@class,'bloom-translationGroup') and descendant::div[@data-book and contains(@class,'bloom-editable')]]";
     var findOnlyBookTitleFields = "descendant-or-self::*[contains(@class,'bloom-translationGroup') and descendant::div[@data-book='bookTitle' and contains(@class,'bloom-editable')]]";
     foreach (XmlElement groupElement in
             pageOrDocumentNode.SafeSelectNodes(findOnlyBookTitleFields))
     {
         foreach (var lang in languageCodes)
         {
             MakeElementWithLanguageForOneGroup(groupElement, lang);
         }
     }
 }
        public XmlNode GetNodeToMerge(XmlNode nodeToMatch, XmlNode parentToSearchIn, HashSet<XmlNode> acceptableTargets)
        {
            if (nodeToMatch == null || parentToSearchIn == null)
                return null;
            var nodeName = nodeToMatch.LocalName;
            var ourForms = nodeToMatch.SafeSelectNodes(nodeName + "/form");

            foreach (XmlNode example in parentToSearchIn.SafeSelectNodes(nodeName))
            {
                if (!acceptableTargets.Contains(example))
                    continue;
                XmlNodeList forms = example.SafeSelectNodes("form");
                if(!SameForms(example, forms, ourForms))
                    continue;

                return example;
            }

            return null; //couldn't find a match
        }
        /// <summary>
        /// We stick 'contentLanguage2' and 'contentLanguage3' classes on editable things in bilingual and trilingual books
        /// </summary>
        public static void UpdateContentLanguageClasses(XmlNode elementOrDom, string vernacularIso, string national1Iso, string national2Iso, string contentLanguageIso2, string contentLanguageIso3)
        {
            var multilingualClass = "bloom-monolingual";
            var contentLanguages = new Dictionary<string, string>();
            contentLanguages.Add(vernacularIso, "bloom-content1");

            if (!String.IsNullOrEmpty(contentLanguageIso2) && vernacularIso != contentLanguageIso2)
            {
                multilingualClass = "bloom-bilingual";
                contentLanguages.Add(contentLanguageIso2, "bloom-content2");
            }
            if (!String.IsNullOrEmpty(contentLanguageIso3) && vernacularIso != contentLanguageIso3 && contentLanguageIso2 != contentLanguageIso3)
            {
                multilingualClass = "bloom-trilingual";
                Debug.Assert(!String.IsNullOrEmpty(contentLanguageIso2), "shouldn't have a content3 lang with no content2 lang");
                contentLanguages.Add(contentLanguageIso3, "bloom-content3");
            }

            //Stick a class in the page div telling the stylesheet how many languages we are displaying (only makes sense for content pages, in Jan 2012).
            foreach (XmlElement pageDiv in elementOrDom.SafeSelectNodes("descendant-or-self::div[contains(@class,'bloom-page') and not(contains(@class,'bloom-frontMatter')) and not(contains(@class,'bloom-backMatter'))]"))
            {
               HtmlDom.RemoveClassesBeginingWith(pageDiv, "bloom-monolingual");
               HtmlDom.RemoveClassesBeginingWith(pageDiv, "bloom-bilingual");
               HtmlDom.RemoveClassesBeginingWith(pageDiv, "bloom-trilingual");
               HtmlDom.AddClassIfMissing(pageDiv, multilingualClass);
            }

            foreach (XmlElement group in elementOrDom.SafeSelectNodes(".//*[contains(@class,'bloom-translationGroup')]"))
            {
                var isXMatter = @group.SafeSelectNodes("ancestor::div[contains(@class,'bloom-frontMatter') or contains(@class,'bloom-backMatter')]").Count > 0;
                foreach (XmlElement e in @group.SafeSelectNodes(".//textarea | .//div")) //nb: we don't necessarily care that a div is editable or not
                {
                    var lang = e.GetAttribute("lang");
                    HtmlDom.RemoveClassesBeginingWith(e, "bloom-content");//they might have been a given content lang before, but not now
                    if (isXMatter && lang == national1Iso)
                    {
                        HtmlDom.AddClass(e, "bloom-contentNational1");
                    }
                    if (isXMatter && !String.IsNullOrEmpty(national2Iso) && lang == national2Iso)
                    {
                        HtmlDom.AddClass(e, "bloom-contentNational2");
                    }
                    foreach (var language in contentLanguages)
                    {
                        if (lang == language.Key)
                        {
                            HtmlDom.AddClass(e, language.Value);
                            break;//don't check the other languages
                        }
                    }
                }
            }
        }
        private static void PrepareElementsOnPageOneLanguage(XmlNode pageDiv, string isoCode)
        {
            foreach (XmlElement groupElement in pageDiv.SafeSelectNodes("descendant-or-self::*[contains(@class,'bloom-translationGroup')]"))
            {
                MakeElementWithLanguageForOneGroup(groupElement, isoCode, "*");
                //remove any elements in the translationgroup which don't have a lang (but ignore any label elements, which we're using for annotating groups)
                foreach (XmlElement elementWithoutLanguage in groupElement.SafeSelectNodes("textarea[not(@lang)] | div[not(@lang) and not(self::label)]"))
                    {
                    elementWithoutLanguage.ParentNode.RemoveChild(elementWithoutLanguage);
                }
            }

            //any editable areas which still don't have a language, set them to the vernacular (this is used for simple templates (non-shell pages))
            foreach (
                XmlElement element in
                    pageDiv.SafeSelectNodes(//NB: the jscript will take items with bloom-editable and set the contentEdtable to true.
                        "descendant-or-self::textarea[not(@lang)] | descendant-or-self::*[(contains(@class, 'bloom-editable') or @contentEditable='true'  or @contenteditable='true') and not(@lang)]")
                )
            {
                element.SetAttribute("lang", isoCode);
            }

            foreach (XmlElement e in pageDiv.SafeSelectNodes("descendant-or-self::*[starts-with(text(),'{')]"))
            {
                foreach (var node in e.ChildNodes)
                {
                    XmlText t = node as XmlText;
                    if (t != null && t.Value.StartsWith("{"))
                        t.Value = "";
                    //otherwise html tidy will throw away spans (at least) that are empty, so we never get a chance to fill in the values.
                }
            }
        }
Exemple #7
0
 //        private static void ClearAwayAllTranslations(XmlNode element)
 //        {
 //
 //            foreach (XmlNode node in element.ChildNodes)//.SafeSelectNodes(String.Format("//*[@lang='{0}']", _collectionSettings.Language1Iso639Code)))
 //            {
 //                if (node.NodeType == XmlNodeType.Text)
 //                {
 //                    node.InnerText = String.Empty;
 //                }
 //                else
 //                {
 //                    ClearAwayAllTranslations(node);
 //                }
 //            }
 //            //after removing text, we could still be left with the line breaks between them
 //            if (element.ChildNodes != null)
 //            {
 //                var possibleBrNodes = new List<XmlNode>();
 //                possibleBrNodes.AddRange(from XmlNode x in element.ChildNodes select x);
 //                foreach (XmlNode node in possibleBrNodes)
 //                {
 //                    if (node.NodeType == XmlNodeType.Element && node.Name.ToLower() == "br")
 //                    {
 //                        node.ParentNode.RemoveChild(node);
 //                    }
 //                }
 //            }
 //        }
 /// <summary>
 /// When building on templates, we usually want to have some sample text, but don't let them bleed through to what the user sees
 /// </summary>
 /// <param name="element"></param>
 private static void ClearAwayDraftText(XmlNode element)
 {
     //clear away everything done in language "x"
     var nodesInLangX = new List<XmlNode>();
     nodesInLangX.AddRange(from XmlNode x in element.SafeSelectNodes(String.Format("//*[@lang='x']")) select x);
     foreach (XmlNode node in nodesInLangX)
     {
         node.ParentNode.RemoveChild(node);
     }
 }
        private static void AddSense(StringBuilder builder, int indentLevel, XmlNode senseNode)
        {
            builder.Append("<tr><td><span class='fieldLabel'>Sense</span></td>");
            var pos = senseNode.SelectSingleNode("grammatical-info");
            if (pos != null)
            {
                builder.AppendFormat("<td><span id='pos'>&nbsp;{0}</span></td>" + Environment.NewLine, pos.GetStringAttribute("value"));
            }
            builder.Append("</tr>");

            foreach (XmlNode def in senseNode.SafeSelectNodes("definition"))
            {
                AddMultiTextHtml(builder,  "definition", def);
            }
            foreach (XmlNode gloss in senseNode.SafeSelectNodes("gloss"))
            {
                AddSingleFormHtml(gloss, builder, "gloss");
            }
            foreach (XmlNode example in senseNode.SafeSelectNodes("example"))
            {
                AddMultiTextHtml(builder, "example", example);
                foreach (XmlNode trans in example.SafeSelectNodes("translation"))
                {
                    AddMultiTextHtml(builder,  "translation", trans);
                }
            }
            foreach (XmlNode field in senseNode.SafeSelectNodes("field"))
            {
                var label = field.GetStringAttribute("type");
                AddMultiTextHtml(builder, label, field);
            }
            foreach (XmlNode node in senseNode.SafeSelectNodes("illustration"))
            {
                builder.AppendFormat("<tr><td><span class='fieldLabel'>illustration</span></td><td>(an image)</td>");
            }
            foreach (XmlNode trait in senseNode.SafeSelectNodes("trait"))
            {
                var label = trait.GetStringAttribute("name");
                var traitValue = trait.GetStringAttribute("value");
                builder.AppendFormat("<tr><td><span class='fieldLabel'>{0}</span></td><td>{1}</td>", label, traitValue);
            }
            foreach (XmlNode note in senseNode.SafeSelectNodes("note"))
            {
                AddMultiTextHtml(builder,  "note", note);
            }
        }
 private static void AddMultiTextHtml(StringBuilder b,  string label, XmlNode node)
 {
     foreach (XmlNode formNode in node.SafeSelectNodes("form"))
     {
         AddSingleFormHtml(formNode, b, label);
     }
 }
Exemple #10
0
        private static void ProcessEntries(XmlWriter writer, IMergeEventListener eventListener, IMergeStrategy mergingStrategy,
			XmlNode ancestorDom, HashSet<string> processedIds,
			XmlNode sourceDom, string sourceLabel, string sourcePath,
			IDictionary<string, XmlNode> otherIdNodeIndex, string otherLabel, string otherPath)
        {
            foreach (XmlNode sourceEntry in sourceDom.SafeSelectNodes("lift/entry"))
            {
                ProcessEntry(writer, eventListener, mergingStrategy, ancestorDom, processedIds,
                             sourceEntry, sourceLabel, sourcePath,
                             otherIdNodeIndex, otherLabel, otherPath);
            }
        }
        /// <summary>
        /// We stick 'contentLanguage2' and 'contentLanguage3' classes on editable things in bilingual and trilingual books
        /// </summary>
        public static void UpdateContentLanguageClasses(XmlNode elementOrDom, CollectionSettings settings,
			string vernacularIso, string contentLanguageIso2, string contentLanguageIso3)
        {
            var multilingualClass = "bloom-monolingual";
            var contentLanguages = new Dictionary<string, string>();
            contentLanguages.Add(vernacularIso, "bloom-content1");

            if (!String.IsNullOrEmpty(contentLanguageIso2) && vernacularIso != contentLanguageIso2)
            {
                multilingualClass = "bloom-bilingual";
                contentLanguages.Add(contentLanguageIso2, "bloom-content2");
            }
            if (!String.IsNullOrEmpty(contentLanguageIso3) && vernacularIso != contentLanguageIso3 &&
                contentLanguageIso2 != contentLanguageIso3)
            {
                multilingualClass = "bloom-trilingual";
                contentLanguages.Add(contentLanguageIso3, "bloom-content3");
                Debug.Assert(!String.IsNullOrEmpty(contentLanguageIso2), "shouldn't have a content3 lang with no content2 lang");
            }

            //Stick a class in the page div telling the stylesheet how many languages we are displaying (only makes sense for content pages, in Jan 2012).
            foreach (
                XmlElement pageDiv in
                    elementOrDom.SafeSelectNodes(
                        "descendant-or-self::div[contains(@class,'bloom-page') and not(contains(@class,'bloom-frontMatter')) and not(contains(@class,'bloom-backMatter'))]")
                )
            {
                HtmlDom.RemoveClassesBeginingWith(pageDiv, "bloom-monolingual");
                HtmlDom.RemoveClassesBeginingWith(pageDiv, "bloom-bilingual");
                HtmlDom.RemoveClassesBeginingWith(pageDiv, "bloom-trilingual");
                HtmlDom.AddClassIfMissing(pageDiv, multilingualClass);
            }

            // This is the "code" part of the visibility system: https://goo.gl/EgnSJo
            foreach (XmlElement group in elementOrDom.SafeSelectNodes(".//*[contains(@class,'bloom-translationGroup')]"))
            {
                var dataDefaultLanguages = HtmlDom.GetAttributeValue(group, "data-default-languages").Split(new char[] { ',', ' ' },
                    StringSplitOptions.RemoveEmptyEntries);

                //nb: we don't necessarily care that a div is editable or not
                foreach (XmlElement e in @group.SafeSelectNodes(".//textarea | .//div"))
                {
                    HtmlDom.RemoveClassesBeginingWith(e, "bloom-content");
                    var lang = e.GetAttribute("lang");

                    //These bloom-content* classes are used by some stylesheet rules, primarily to boost the font-size of some languages.
                    //Enhance: this is too complex; the semantics of these overlap with each other and with bloom-visibility-code-on, and with data-language-order.
                    //It would be better to have non-overlapping things; 1 for order, 1 for visibility, one for the lang's role in this collection.
                    string orderClass;
                    if (contentLanguages.TryGetValue(lang, out orderClass))
                    {
                        HtmlDom.AddClass(e, orderClass); //bloom-content1, bloom-content2, bloom-content3
                    }

                    //Enhance: it's even more likely that we can get rid of these by replacing them with bloom-content2, bloom-content3
                    if (lang == settings.Language2Iso639Code)
                    {
                        HtmlDom.AddClass(e, "bloom-contentNational1");
                    }
                    if (lang == settings.Language3Iso639Code)
                    {
                        HtmlDom.AddClass(e, "bloom-contentNational2");
                    }

                    HtmlDom.RemoveClassesBeginingWith(e, "bloom-visibility-code");
                    if (ShouldNormallyShowEditable(lang, dataDefaultLanguages, contentLanguageIso2, contentLanguageIso3, settings))
                    {
                        HtmlDom.AddClass(e, "bloom-visibility-code-on");
                    }

                    UpdateRightToLeftSetting(settings, e, lang);
                }
            }
        }
Exemple #12
0
        /// <summary>
        /// walk throught the sourceDom, collecting up values from elements that have data-book or data-collection attributes.
        /// </summary>
        private void GatherDataItemsFromXElement(DataSet data, XmlNode sourceElement
			/* can be the whole sourceDom or just a page */)
        {
            string elementName = "*";
            try
            {
                string query = String.Format(".//{0}[(@data-book or @data-library or @data-collection)]", elementName);

                XmlNodeList nodesOfInterest = sourceElement.SafeSelectNodes(query);

                foreach (XmlElement node in nodesOfInterest)
                {
                    bool isCollectionValue = false;

                    string key = node.GetAttribute("data-book").Trim();
                    if (key == String.Empty)
                    {
                        key = node.GetAttribute("data-collection").Trim();
                        if (key == String.Empty)
                        {
                            key = node.GetAttribute("data-library").Trim(); //the old (pre-version 1) name of collections was 'library'
                        }
                        isCollectionValue = true;
                    }

                    string value = node.InnerXml.Trim(); //may contain formatting
                    if (node.Name.ToLower() == "img")
                    {
                        value = node.GetAttribute("src");
                        //Make the name of the image safe for showing up in raw html (not just in the relatively safe confines of the src attribut),
                        //becuase it's going to show up between <div> tags.  E.g. "Land & Water.png" as the cover page used to kill us.
                        value = WebUtility.HtmlEncode(WebUtility.HtmlDecode(value));
                    }
                    if (!String.IsNullOrEmpty(value) && !value.StartsWith("{"))
                        //ignore placeholder stuff like "{Book Title}"; that's not a value we want to collect
                    {
                        string lang = node.GetOptionalStringAttribute("lang", "*");
                        if (lang == "") //the above doesn't stop a "" from getting through
                            lang = "*";
                        if ((elementName.ToLower() == "textarea" || elementName.ToLower() == "input" ||
                             node.GetOptionalStringAttribute("contenteditable", "false") == "true") &&
                            (lang == "V" || lang == "N1" || lang == "N2"))
                        {
                            throw new ApplicationException(
                                "Editable element (e.g. TextArea) should not have placeholder @lang attributes (V,N1,N2)\r\n\r\n" +
                                node.OuterXml);
                        }

                        //if we don't have a value for this variable and this language, add it
                        if (!data.TextVariables.ContainsKey(key))
                        {
                            var t = new MultiTextBase();
                            t.SetAlternative(lang, value);
                            data.TextVariables.Add(key, new NamedMutliLingualValue(t, isCollectionValue));
                        }
                        else if (!data.TextVariables[key].TextAlternatives.ContainsAlternative(lang))
                        {
                            MultiTextBase t = data.TextVariables[key].TextAlternatives;
                            t.SetAlternative(lang, value);
                        }
                    }
                }
            }
            catch (Exception error)
            {
                throw new ApplicationException(
                    "Error in GatherDataItemsFromDom(," + elementName + "). RawDom was:\r\n" + sourceElement.OuterXml,
                    error);
            }
        }
Exemple #13
0
        public void UpdatePageSplitMode(XmlNode node)
        {
            //NB: this can currently only split pages, not move them together. Doable, just not called for by the UI or unit tested yet.

            if (ElementDistribution == ElementDistributionChoices.CombinedPages)
                return;

            var combinedPages = node.SafeSelectNodes("descendant-or-self::div[contains(@class,'bloom-combinedPage')]");
            foreach (XmlElement pageDiv in combinedPages)
            {
                XmlElement trailer = (XmlElement) pageDiv.CloneNode(true);
                pageDiv.ParentNode.InsertAfter(trailer, pageDiv);

                pageDiv.SetAttribute("class", pageDiv.GetAttribute("class").Replace("bloom-combinedPage", "bloom-leadingPage"));
                var leader = pageDiv;
                trailer.SetAttribute("class", trailer.GetAttribute("class").Replace("bloom-combinedPage", "bloom-trailingPage"));

                //give all new ids to both pages

                leader.SetAttribute("id", Guid.NewGuid().ToString());
                trailer.SetAttribute("id", Guid.NewGuid().ToString());

                //now split the elements

                foreach (XmlElement div in leader.SafeSelectNodes("descendant-or-self::*[contains(@class, 'bloom-trailingElement')]"))
                {
                    div.ParentNode.RemoveChild(div);
                }

                foreach (XmlElement div in trailer.SafeSelectNodes("descendant-or-self::*[contains(@class, 'bloom-leadingElement')]"))
                {
                    div.ParentNode.RemoveChild(div);
                }
            }
        }
Exemple #14
0
        // records key, lang pairs for which we found an empty element in the source.
        /// <summary>
        /// walk through the sourceDom, collecting up values from elements that have data-book or data-collection or data-book-attributes attributes.
        /// </summary>
        private void GatherDataItemsFromXElement(DataSet data,
			XmlNode sourceElement, // can be the whole sourceDom or just a page
			HashSet<Tuple<string, string>> itemsToDelete = null)
        {
            string elementName = "*";
            try
            {
                string query = String.Format(".//{0}[(@data-book or @data-library or @data-collection or @data-book-attributes) and not(contains(@class,'bloom-writeOnly'))]", elementName);

                XmlNodeList nodesOfInterest = sourceElement.SafeSelectNodes(query);

                foreach (XmlElement node in nodesOfInterest)
                {
                    bool isCollectionValue = false;

                    string key = node.GetAttribute("data-book").Trim();
                    if (key == String.Empty)
                    {
                        key = node.GetAttribute("data-book-attributes").Trim();
                        if (key != String.Empty)
                        {
                            GatherAttributes(data, node, key);
                            continue;
                        }
                        key = node.GetAttribute("data-collection").Trim();
                        if (key == String.Empty)
                        {
                            key = node.GetAttribute("data-library").Trim(); //the old (pre-version 1) name of collections was 'library'
                        }
                        isCollectionValue = true;
                    }

                    string value;
                    if (HtmlDom.IsImgOrSomethingWithBackgroundImage(node))
                    {
                        value = HtmlDom.GetImageElementUrl(new ElementProxy(node)).UrlEncoded;
                        KeysOfVariablesThatAreUrlEncoded.Add(key);
                    }
                    else
                    {
                        var node1 = node.CloneNode(true); // so we can remove labels without modifying node
                        // Datadiv content should be node content without labels. The labels are not really part
                        // of the content we want to replicate, they are just information for the user, and
                        // specific to one context. Also, including them causes them to get repeated in each location;
                        // SetInnerXmlPreservingLabel() assumes data set content does not include label elements.
                        var labels = node1.SafeSelectNodes(".//label").Cast<XmlElement>().ToList();
                        foreach (var label in labels)
                            label.ParentNode.RemoveChild(label);
                        value = node1.InnerXml.Trim(); //may contain formatting
                        if(KeysOfVariablesThatAreUrlEncoded.Contains(key))
                        {
                            value = UrlPathString.CreateFromHtmlXmlEncodedString(value).UrlEncoded;
                        }
                    }

                    string lang = node.GetOptionalStringAttribute("lang", "*");
                    if (lang == "") //the above doesn't stop a "" from getting through
                        lang = "*";
                    if (lang == "{V}")
                        lang = _collectionSettings.Language1Iso639Code;
                    if(lang == "{N1}")
                        lang = _collectionSettings.Language2Iso639Code;
                    if(lang == "{N2}")
                        lang = _collectionSettings.Language3Iso639Code;

                    if (string.IsNullOrEmpty(value))
                    {
                        // This is a value we may want to delete
                        if (itemsToDelete != null)
                            itemsToDelete.Add(Tuple.Create(key, lang));
                    }
                    else if (!value.StartsWith("{"))
                        //ignore placeholder stuff like "{Book Title}"; that's not a value we want to collect
                    {
                        if ((elementName.ToLowerInvariant() == "textarea" || elementName.ToLowerInvariant() == "input" ||
                             node.GetOptionalStringAttribute("contenteditable", "false") == "true") &&
                            (lang == "V" || lang == "N1" || lang == "N2"))
                        {
                            throw new ApplicationException(
                                "Editable element (e.g. TextArea) should not have placeholder @lang attributes (V,N1,N2)\r\n\r\n" +
                                node.OuterXml);
                        }

                        //if we don't have a value for this variable and this language, add it
                        if (!data.TextVariables.ContainsKey(key))
                        {
                            var t = new MultiTextBase();
                            t.SetAlternative(lang, value);
                            data.TextVariables.Add(key, new NamedMutliLingualValue(t, isCollectionValue));
                        }
                        else if (!data.TextVariables[key].TextAlternatives.ContainsAlternative(lang))
                        {
                            MultiTextBase t = data.TextVariables[key].TextAlternatives;
                            t.SetAlternative(lang, value);
                        }
                    }

                    if (KeysOfVariablesThatAreUrlEncoded.Contains(key))
                    {
                        Debug.Assert(!value.Contains("&amp;"), "In memory, all image urls should be encoded such that & is just &.");
                    }

                }
            }
            catch (Exception error)
            {
                throw new ApplicationException(
                    "Error in GatherDataItemsFromDom(," + elementName + "). RawDom was:\r\n" + sourceElement.OuterXml,
                    error);
            }
        }
        public static void UpdatePageSizeAndOrientationClasses(XmlNode node, Layout layout)
        {
            foreach (XmlElement pageDiv in node.SafeSelectNodes("descendant-or-self::div[contains(@class,'bloom-page')]"))
            {
                RemoveClassesContaining(pageDiv, "layout-");
                RemoveClassesContaining(pageDiv, "Landscape");
                RemoveClassesContaining(pageDiv, "Portrait");

                foreach (var cssClassName in layout.ClassNames)
                {
                    AddClass(pageDiv, cssClassName);
                }
            }
        }