MegaBites-AI/Windows-powershell
0372
1// Copyright (c) Microsoft Corporation.2// Licensed under the MIT License.3 4using System.Collections;5using System.Collections.ObjectModel;6using System.Globalization;7using System.Reflection;8using System.Text;9using System.Xml;10 11namespace System.Management.Automation12{13 /// <summary>14 /// MamlNode is an xml node in MAML schema. Maml schema includes formatting oriented tags like para, list15 /// etc, which needs to be taken care of during display. As a result, xml node in Maml schema can't be16 /// converted into PSObject directly with XmlNodeAdapter.17 ///18 /// MamlNode class provides logic in converting formatting tags into the format acceptable by monad format19 /// and output engine.20 ///21 /// Following three kinds of formating tags are supported per our agreement with Maml team,22 /// 1. para,23 /// <para>24 /// para text here25 /// </para>26 /// 2. list,27 /// <list class="ordered|unordered">28 /// <listItem>29 /// <para>30 /// listItem Text here31 /// </para>32 /// </listItem>33 /// </list>34 /// 3. definition list,35 /// <definitionList>36 /// <definitionListItem>37 /// <term>38 /// definition term text here39 /// </term>40 /// <definition>41 /// <para>42 /// definition text here43 /// </para>44 /// </definition>45 /// </definitionListItem>46 /// </definitionList>47 /// After processing, content of these three tags will be converted into textItem and its derivations,48 /// 1. para => paraTextItem49 /// <textItem class="paraTextItem">50 /// <text>para text here</text>51 /// </textItem>52 /// 2. list => a list of listTextItem's (which can be ordered or unordered)53 /// <textItem class="unorderedListTextItem">54 /// <tag>*</tag>55 /// <text>text for list item 1</text>56 /// </textItem>57 /// <textItem class="unorderedListTextItem">58 /// <tag>*</tag>59 /// <text>text for list item 2</text>60 /// </textItem>61 /// 3. definitionList => a list of definitionTextItem's62 /// <definitionListItem>63 /// <term>definition term here</term>64 /// <definition>definition text here</definition>65 /// </definitionListItem>66 /// </summary>67 internal class MamlNode68 {69 /// <summary>70 /// Constructor for HelpInfo.71 /// </summary>72 internal MamlNode(XmlNode xmlNode)73 {74 _xmlNode = xmlNode;75 }76 77 private readonly XmlNode _xmlNode;78 79 /// <summary>80 /// Underline xmlNode for this MamlNode object.81 /// </summary>82 /// <value></value>83 internal XmlNode XmlNode84 {85 get86 {87 return _xmlNode;88 }89 }90 91 private PSObject _mshObject;92 93 /// <summary>94 /// MshObject which is converted from XmlNode.95 /// </summary>96 /// <value></value>97 internal PSObject PSObject98 {99 get100 {101 if (_mshObject == null)102 {103 // There is no XSLT to convert docs to supported maml format104 // We dont want comments etc to spoil our format.105 // So remove all unsupported nodes before constructing help106 // object.107 RemoveUnsupportedNodes(_xmlNode);108 _mshObject = GetPSObject(_xmlNode);109 }110 111 return _mshObject;112 }113 }114 115 #region Conversion of xmlNode => PSObject116 117 /// <summary>118 /// Convert an xmlNode into an PSObject. There are four scenarios,119 /// 1. Null xml, this will return an PSObject wrapping a null object.120 /// 2. Atomic xml, which is an xmlNode with only one simple text child node121 /// <atomicXml attribute="value">122 /// atomic xml text123 /// </atomicXml>124 /// In this case, an PSObject that wraps string "atomic xml text" will be returned with following properties125 /// attribute => name126 /// 3. Composite xml, which is an xmlNode with structured child nodes, but not a special case for Maml formatting.127 /// <compositeXml attribute="attribute">128 /// <singleChildNode>129 /// single child node text130 /// </singleChildNode>131 /// <dupChildNode>132 /// dup child node text 1133 /// </dupChildNode>134 /// <dupChildNode>135 /// dup child node text 2136 /// </dupChildNode>137 /// </compositeXml>138 /// In this case, an PSObject will base generated based on an inside PSObject,139 /// which in turn has following properties140 /// a. property "singleChildNode", with its value an PSObject wrapping string "single child node text"141 /// b. property "dupChildNode", with its value an PSObject array wrapping strings for two dupChildNode's142 /// The outside PSObject will have property,143 /// a. property "attribute", with its value an PSObject wrapping string "attribute"144 /// 4. Maml formatting xml, this is a special case for Composite xml, for example145 /// <description attribute="value">146 /// <para>147 /// para 1148 /// </para>149 /// <list>150 /// <listItem>151 /// <para>152 /// list item 1153 /// </para>154 /// </listItem>155 /// <listItem>156 /// <para>157 /// list item 2158 /// </para>159 /// </listItem>160 /// </list>161 /// <definitionList>162 /// <definitionListItem>163 /// <term>164 /// term 1165 /// </term>166 /// <definition>167 /// definition list item 1168 /// </definition>169 /// </definitionListItem>170 /// <definitionListItem>171 /// <term>172 /// term 2173 /// </term>174 /// <definition>175 /// definition list item 2176 /// </definition>177 /// </definitionListItem>178 /// </definitionList>179 /// </description>180 /// In this case, an PSObject based on an PSObject array will be created. The inside PSObject array181 /// will contain following items182 /// . a MamlParaTextItem based on "para 1"183 /// . a MamlUnorderedListItem based on "list item 1"184 /// . a MamlUnorderedListItem based on "list item 2"185 /// . a MamlDefinitionListItem based on "definition list item 1"186 /// . a MamlDefinitionListItem based on "definition list item 2"187 ///188 /// The outside PSObject will have a property189 /// attribute => "value"190 /// </summary>191 /// <param name="xmlNode"></param>192 /// <returns></returns>193 private PSObject GetPSObject(XmlNode xmlNode)194 {195 if (xmlNode == null)196 return new PSObject();197 198 PSObject mshObject = null;199 200 if (IsAtomic(xmlNode))201 {202 mshObject = new PSObject(xmlNode.InnerText.Trim());203 }204 else if (IncludeMamlFormatting(xmlNode))205 {206 mshObject = new PSObject(GetMamlFormattingPSObjects(xmlNode));207 }208 else209 {210 mshObject = new PSObject(GetInsidePSObject(xmlNode));211 // Add typeNames to this MSHObject and create views so that212 // the output is readable. This is done only for complex nodes.213 mshObject.TypeNames.Clear();214 215 if (xmlNode.Attributes["type"] != null)216 {217 if (string.Equals(xmlNode.Attributes["type"].Value, "field", StringComparison.OrdinalIgnoreCase))218 mshObject.TypeNames.Add("MamlPSClassHelpInfo#field");219 else if (string.Equals(xmlNode.Attributes["type"].Value, "method", StringComparison.OrdinalIgnoreCase))220 mshObject.TypeNames.Add("MamlPSClassHelpInfo#method");221 }222 223 mshObject.TypeNames.Add("MamlCommandHelpInfo#" + xmlNode.LocalName);224 }225 226 if (xmlNode.Attributes != null)227 {228 foreach (XmlNode attribute in xmlNode.Attributes)229 {230 mshObject.Properties.Add(new PSNoteProperty(attribute.Name, attribute.Value));231 }232 }233 234 return mshObject;235 }236 237 /// <summary>238 /// Get inside PSObject created based on inside nodes of xmlNode.239 ///240 /// The inside PSObject will be based on null. It will created one241 /// property per inside node grouping by node names.242 ///243 /// For example, for xmlNode like,244 /// <command>245 /// <name>get-item</name>246 /// <note>note 1</note>247 /// <note>note 2</note>248 /// </command>249 /// It will create an PSObject based on null, with following two properties250 /// . property 1: name="name" value=an PSObject to wrap string "get-item"251 /// . property 2: name="note" value=an PSObject array with following two PSObjects252 /// 1. PSObject wrapping string "note 1"253 /// 2. PSObject wrapping string "note 2"254 /// </summary>255 /// <param name="xmlNode"></param>256 /// <returns></returns>257 private PSObject GetInsidePSObject(XmlNode xmlNode)258 {259 Hashtable properties = GetInsideProperties(xmlNode);260 261 PSObject mshObject = new PSObject();262 263 IDictionaryEnumerator enumerator = properties.GetEnumerator();264 265 while (enumerator.MoveNext())266 {267 mshObject.Properties.Add(new PSNoteProperty((string)enumerator.Key, enumerator.Value));268 }269 270 return mshObject;271 }272 273 /// <summary>274 /// This is for getting inside properties of an XmlNode. Properties are275 /// stored in a hashtable with key as property name and value as property value.276 ///277 /// Inside node with same node names will be grouped into one property with278 /// property value as an array.279 ///280 /// For example, for xmlNode like,281 /// <command>282 /// <name>get-item</name>283 /// <note>note 1</note>284 /// <note>note 2</note>285 /// </command>286 /// It will create an PSObject based on null, with following two properties287 /// . property 1: name="name" value=an PSObject to wrap string "get-item"288 /// . property 2: name="note" value=an PSObject array with following two PSObjects289 /// 1. PSObject wrapping string "note 1"290 /// 2. PSObject wrapping string "note 2"291 ///292 /// Since we don't know whether an node name will be used more than once,293 /// We are making each property value is an array (PSObject[]) to start with.294 /// At the end, SimplifyProperties will be called to reduce PSObject[] containing295 /// only one element to PSObject itself.296 /// </summary>297 /// <param name="xmlNode"></param>298 /// <returns></returns>299 private Hashtable GetInsideProperties(XmlNode xmlNode)300 {301 Hashtable properties = new Hashtable(StringComparer.OrdinalIgnoreCase);302 303 if (xmlNode == null)304 return properties;305 306 if (xmlNode.ChildNodes != null)307 {308 foreach (XmlNode childNode in xmlNode.ChildNodes)309 {310 AddProperty(properties, childNode.LocalName, GetPSObject(childNode));311 }312 }313 314 return SimplifyProperties(properties);315 }316 317 /// <summary>318 /// Removes unsupported child nodes recursively from the given319 /// xml node so that they wont spoil the format.320 /// </summary>321 /// <param name="xmlNode">322 /// Node whose children are verified for maml.323 /// </param>324 private static void RemoveUnsupportedNodes(XmlNode xmlNode)325 {326 // Start with the first child..327 // We want to modify only children..328 // The current node is taken care by the callee..329 XmlNode childNode = xmlNode.FirstChild;330 while (childNode != null)331 {332 // We dont want Comments..so remove..333 if (childNode.NodeType == XmlNodeType.Comment)334 {335 XmlNode nodeToRemove = childNode;336 childNode = childNode.NextSibling;337 // Remove this node and its children if any..338 xmlNode.RemoveChild(nodeToRemove);339 }340 else341 {342 // Search children...343 RemoveUnsupportedNodes(childNode);344 childNode = childNode.NextSibling;345 }346 }347 }348 349 /// <summary>350 /// This is for adding a property into a property hashtable.351 ///352 /// As mentioned in comment of GetInsideProperties, property values stored in353 /// property hashtable is an array to begin with.354 ///355 /// The property value to be added is an mshObject whose base object can be an356 /// PSObject array itself. In that case, each PSObject in the array will be357 /// added separately into the property value array. This case can only happen when358 /// an node with maml formatting node inside is treated. The side effect of this359 /// is that the properties for outside mshObject will be lost. An example of this360 /// is that,361 /// <command>362 /// <description attrib1="value1">363 /// <para></para>364 /// <list></list>365 /// <definitionList></definitionList>366 /// </description>367 /// </command>368 /// After the processing, PSObject corresponding to command will have an property369 /// with name "description" and a value of an PSObject array created based on370 /// maml formatting node inside "description" node. The attribute of description node371 /// "attrib1" will be lost. This seems to be OK with current practice of authoring372 /// monad command help.373 /// </summary>374 /// <param name="properties">Property hashtable.</param>375 /// <param name="name">Property name.</param>376 /// <param name="mshObject">Property value.</param>377 private static void AddProperty(Hashtable properties, string name, PSObject mshObject)378 {379 ArrayList propertyValues = (ArrayList)properties[name];380 381 if (propertyValues == null)382 {383 propertyValues = new ArrayList();384 385 properties[name] = propertyValues;386 }387 388 if (mshObject == null)389 return;390 391 if (mshObject.BaseObject is PSCustomObject || !mshObject.BaseObject.GetType().Equals(typeof(PSObject[])))392 {393 propertyValues.Add(mshObject);394 return;395 }396 397 PSObject[] mshObjects = (PSObject[])mshObject.BaseObject;398 399 for (int i = 0; i < mshObjects.Length; i++)400 {401 propertyValues.Add(mshObjects[i]);402 }403 404 return;405 }406 407 /// <summary>408 /// This is for simplifying property value array of only one element.409 ///410 /// As mentioned in comments for GetInsideProperties, this is needed411 /// to reduce an array of only one PSObject into the PSObject itself.412 ///413 /// A side effect of this function is to turn property values from414 /// ArrayList into PSObject[].415 /// </summary>416 /// <param name="properties"></param>417 /// <returns></returns>418 private static Hashtable SimplifyProperties(Hashtable properties)419 {420 if (properties == null)421 return null;422 423 Hashtable result = new Hashtable(StringComparer.OrdinalIgnoreCase);424 IDictionaryEnumerator enumerator = properties.GetEnumerator();425 426 while (enumerator.MoveNext())427 {428 ArrayList propertyValues = (ArrayList)enumerator.Value;429 430 if (propertyValues == null || propertyValues.Count == 0)431 continue;432 433 if (propertyValues.Count == 1)434 {435 if (!IsMamlFormattingPSObject((PSObject)propertyValues[0]))436 {437 PSObject mshObject = (PSObject)propertyValues[0];438 439 // Even for strings or other basic types, they need to be contained in PSObject in case440 // there is attributes for this object.441 442 result[enumerator.Key] = mshObject;443 444 continue;445 }446 }447 448 result[enumerator.Key] = propertyValues.ToArray(typeof(PSObject));449 }450 451 return result;452 }453 454 /// <summary>455 /// An xmlNode is atomic if it contains no structured inside nodes.456 /// </summary>457 /// <param name="xmlNode"></param>458 /// <returns></returns>459 private static bool IsAtomic(XmlNode xmlNode)460 {461 if (xmlNode == null)462 return false;463 464 if (xmlNode.ChildNodes == null)465 return true;466 467 if (xmlNode.ChildNodes.Count > 1)468 return false;469 470 if (xmlNode.ChildNodes.Count == 0)471 return true;472 473 if (xmlNode.ChildNodes[0].GetType().Equals(typeof(XmlText)))474 return true;475 476 return false;477 }478 479 #endregion480 481 #region Maml formatting482 483 /// <summary>484 /// Check whether an xmlNode contains childnodes which is for485 /// maml formatting.486 /// </summary>487 /// <param name="xmlNode"></param>488 /// <returns></returns>489 private static bool IncludeMamlFormatting(XmlNode xmlNode)490 {491 if (xmlNode == null)492 return false;493 494 if (xmlNode.ChildNodes == null || xmlNode.ChildNodes.Count == 0)495 return false;496 497 foreach (XmlNode childNode in xmlNode.ChildNodes)498 {499 if (IsMamlFormattingNode(childNode))500 {501 return true;502 }503 }504 505 return false;506 }507 508 /// <summary>509 /// Check whether a node is for maml formatting. This include following nodes,510 /// a. para511 /// b. list512 /// c. definitionList.513 /// </summary>514 /// <param name="xmlNode"></param>515 /// <returns></returns>516 private static bool IsMamlFormattingNode(XmlNode xmlNode)517 {518 if (xmlNode.LocalName.Equals("para", StringComparison.OrdinalIgnoreCase))519 return true;520 521 if (xmlNode.LocalName.Equals("list", StringComparison.OrdinalIgnoreCase))522 return true;523 524 if (xmlNode.LocalName.Equals("definitionList", StringComparison.OrdinalIgnoreCase))525 return true;526 527 return false;528 }529 530 /// <summary>531 /// Check whether an mshObject is created from a maml formatting node.532 /// </summary>533 /// <param name="mshObject"></param>534 /// <returns></returns>535 private static bool IsMamlFormattingPSObject(PSObject mshObject)536 {537 Collection<string> typeNames = mshObject.TypeNames;538 539 if (typeNames == null || typeNames.Count == 0)540 return false;541 542 return typeNames[typeNames.Count - 1].Equals("MamlTextItem", StringComparison.OrdinalIgnoreCase);543 }544 545 /// <summary>546 /// Convert an xmlNode containing maml formatting nodes into an PSObject array.547 ///548 /// For example, for node,549 /// <description attribute="value">550 /// <para>551 /// para 1552 /// </para>553 /// <list>554 /// <listItem>555 /// <para>556 /// list item 1557 /// </para>558 /// </listItem>559 /// <listItem>560 /// <para>561 /// list item 2562 /// </para>563 /// </listItem>564 /// </list>565 /// <definitionList>566 /// <definitionListItem>567 /// <term>568 /// term 1569 /// </term>570 /// <definition>571 /// definition list item 1572 /// </definition>573 /// </definitionListItem>574 /// <definitionListItem>575 /// <term>576 /// term 2577 /// </term>578 /// <definition>579 /// definition list item 2580 /// </definition>581 /// </definitionListItem>582 /// </definitionList>583 /// </description>584 /// In this case, an PSObject based on an PSObject array will be created. The inside PSObject array585 /// will contain following items586 /// . a MamlParaTextItem based on "para 1"587 /// . a MamlUnorderedListItem based on "list item 1"588 /// . a MamlUnorderedListItem based on "list item 2"589 /// . a MamlDefinitionListItem based on "definition list item 1"590 /// . a MamlDefinitionListItem based on "definition list item 2"591 /// </summary>592 /// <param name="xmlNode"></param>593 /// <returns></returns>594 private PSObject[] GetMamlFormattingPSObjects(XmlNode xmlNode)595 {596 ArrayList mshObjects = new ArrayList();597 598 int paraNodes = GetParaMamlNodeCount(xmlNode.ChildNodes);599 int count = 0;600 // Don't trim the content if this is an "introduction" node.601 bool trim = !string.Equals(xmlNode.Name, "maml:introduction", StringComparison.OrdinalIgnoreCase);602 foreach (XmlNode childNode in xmlNode.ChildNodes)603 {604 if (childNode.LocalName.Equals("para", StringComparison.OrdinalIgnoreCase))605 {606 ++count;607 PSObject paraPSObject = GetParaPSObject(childNode, count != paraNodes, trim: trim);608 if (paraPSObject != null)609 mshObjects.Add(paraPSObject);610 continue;611 }612 613 if (childNode.LocalName.Equals("list", StringComparison.OrdinalIgnoreCase))614 {615 ArrayList listPSObjects = GetListPSObjects(childNode);616 617 for (int i = 0; i < listPSObjects.Count; i++)618 {619 mshObjects.Add(listPSObjects[i]);620 }621 622 continue;623 }624 625 if (childNode.LocalName.Equals("definitionList", StringComparison.OrdinalIgnoreCase))626 {627 ArrayList definitionListPSObjects = GetDefinitionListPSObjects(childNode);628 629 for (int i = 0; i < definitionListPSObjects.Count; i++)630 {631 mshObjects.Add(definitionListPSObjects[i]);632 }633 634 continue;635 }636 637 // If we get here, there is some tags that is not supported by maml.638 WriteMamlInvalidChildNodeError(xmlNode, childNode);639 }640 641 return (PSObject[])mshObjects.ToArray(typeof(PSObject));642 }643 644 /// <summary>645 /// Gets the number of para nodes.646 /// </summary>647 /// <param name="nodes"></param>648 /// <returns></returns>649 private static int GetParaMamlNodeCount(XmlNodeList nodes)650 {651 int i = 0;652 653 foreach (XmlNode childNode in nodes)654 {655 if (childNode.LocalName.Equals("para", StringComparison.OrdinalIgnoreCase))656 {657 if (childNode.InnerText.Trim().Equals(string.Empty))658 {659 continue;660 }661 662 ++i;663 }664 }665 666 return i;667 }668 669 /// <summary>670 /// Write an error to helpsystem to indicate an invalid maml child node.671 /// </summary>672 /// <param name="node"></param>673 /// <param name="childNode"></param>674 private void WriteMamlInvalidChildNodeError(XmlNode node, XmlNode childNode)675 {676 ErrorRecord errorRecord = new ErrorRecord(new ParentContainsErrorRecordException("MamlInvalidChildNodeError"), "MamlInvalidChildNodeError", ErrorCategory.SyntaxError, null);677 errorRecord.ErrorDetails = new ErrorDetails(typeof(MamlNode).Assembly, "HelpErrors", "MamlInvalidChildNodeError", node.LocalName, childNode.LocalName, GetNodePath(node));678 this.Errors.Add(errorRecord);679 }680 681 /// <summary>682 /// Write an error to help system to indicate an invalid child node count.683 /// </summary>684 /// <param name="node"></param>685 /// <param name="childNodeName"></param>686 /// <param name="count"></param>687 private void WriteMamlInvalidChildNodeCountError(XmlNode node, string childNodeName, int count)688 {689 ErrorRecord errorRecord = new ErrorRecord(new ParentContainsErrorRecordException("MamlInvalidChildNodeCountError"), "MamlInvalidChildNodeCountError", ErrorCategory.SyntaxError, null);690 errorRecord.ErrorDetails = new ErrorDetails(typeof(MamlNode).Assembly, "HelpErrors", "MamlInvalidChildNodeCountError", node.LocalName, childNodeName, count, GetNodePath(node));691 this.Errors.Add(errorRecord);692 }693 694 private static string GetNodePath(XmlNode xmlNode)695 {696 if (xmlNode == null)697 return string.Empty;698 699 if (xmlNode.ParentNode == null)700 return "\\" + xmlNode.LocalName;701 702 return GetNodePath(xmlNode.ParentNode) + "\\" + xmlNode.LocalName + GetNodeIndex(xmlNode);703 }704 705 private static string GetNodeIndex(XmlNode xmlNode)706 {707 if (xmlNode == null || xmlNode.ParentNode == null)708 return string.Empty;709 710 int index = 0;711 int total = 0;712 713 foreach (XmlNode siblingNode in xmlNode.ParentNode.ChildNodes)714 {715 if (siblingNode == xmlNode)716 {717 index = total++;718 continue;719 }720 721 if (siblingNode.LocalName.Equals(xmlNode.LocalName, StringComparison.OrdinalIgnoreCase))722 {723 total++;724 }725 }726 727 if (total > 1)728 {729 return "[" + index.ToString("d", CultureInfo.CurrentCulture) + "]";730 }731 732 return string.Empty;733 }734 735 /// <summary>736 /// Convert a para node into an mshObject.737 ///738 /// For example,739 /// <para>740 /// para text741 /// </para>742 /// In this case, an PSObject of type "MamlParaTextItem" will be created with following property743 /// a. text="para text"744 /// </summary>745 /// <param name="xmlNode"></param>746 /// <param name="newLine"></param>747 /// <param name="trim"></param>748 /// <returns></returns>749 private static PSObject GetParaPSObject(XmlNode xmlNode, bool newLine, bool trim = true)750 {751 if (xmlNode == null)752 return null;753 754 if (!xmlNode.LocalName.Equals("para", StringComparison.OrdinalIgnoreCase))755 return null;756 757 PSObject mshObject = new PSObject();758 759 StringBuilder sb = new StringBuilder();760 761 if (newLine && !xmlNode.InnerText.Trim().Equals(string.Empty))762 {763 sb.AppendLine(xmlNode.InnerText.Trim());764 }765 else766 {767 var innerText = xmlNode.InnerText;768 if (trim)769 {770 innerText = innerText.Trim();771 }772 773 sb.Append(innerText);774 }775 776 mshObject.Properties.Add(new PSNoteProperty("Text", sb.ToString()));777 778 mshObject.TypeNames.Clear();779 mshObject.TypeNames.Add("MamlParaTextItem");780 mshObject.TypeNames.Add("MamlTextItem");781 782 return mshObject;783 }784 785 /// <summary>786 /// Convert a list node into an PSObject array.787 ///788 /// For example,789 /// <list class="ordered">790 /// <listItem>791 /// <para>792 /// text for list item 1793 /// </para>794 /// </listItem>795 /// <listItem>796 /// <para>797 /// text for list item 2798 /// </para>799 /// </listItem>800 /// </list>801 /// In this case, an array of PSObject, each of type "MamlOrderedListText" will be created with following802 /// two properties,803 /// a. tag=" 1. " or " 2. "804 /// b. text="text for list item 1" or "text for list item 2"805 /// In the case of unordered list, similar PSObject will created with type to be "MamlUnorderedListText" and tag="*"806 /// </summary>807 /// <param name="xmlNode"></param>808 /// <returns></returns>809 private ArrayList GetListPSObjects(XmlNode xmlNode)810 {811 ArrayList mshObjects = new ArrayList();812 813 if (xmlNode == null)814 return mshObjects;815 816 if (!xmlNode.LocalName.Equals("list", StringComparison.OrdinalIgnoreCase))817 return mshObjects;818 819 if (xmlNode.ChildNodes == null || xmlNode.ChildNodes.Count == 0)820 return mshObjects;821 822 bool ordered = IsOrderedList(xmlNode);823 int index = 1;824 825 foreach (XmlNode childNode in xmlNode.ChildNodes)826 {827 if (childNode.LocalName.Equals("listItem", StringComparison.OrdinalIgnoreCase))828 {829 PSObject listItemPSObject = GetListItemPSObject(childNode, ordered, ref index);830 831 if (listItemPSObject != null)832 mshObjects.Add(listItemPSObject);833 834 continue;835 }836 837 // If we get here, there is some tags that is not supported by maml.838 WriteMamlInvalidChildNodeError(xmlNode, childNode);839 }840 841 return mshObjects;842 }843 844 /// <summary>845 /// Check whether a list is ordered or not.846 /// </summary>847 /// <param name="xmlNode"></param>848 /// <returns></returns>849 private static bool IsOrderedList(XmlNode xmlNode)850 {851 if (xmlNode == null)852 return false;853 854 if (xmlNode.Attributes == null || xmlNode.Attributes.Count == 0)855 return false;856 857 foreach (XmlNode attribute in xmlNode.Attributes)858 {859 if (attribute.Name.Equals("class", StringComparison.OrdinalIgnoreCase)860 && attribute.Value.Equals("ordered", StringComparison.OrdinalIgnoreCase))861 {862 return true;863 }864 }865 866 return false;867 }868 869 /// <summary>870 /// Convert an listItem node into an PSObject with property "tag" and "text"871 /// </summary>872 /// <param name="xmlNode"></param>873 /// <param name="ordered"></param>874 /// <param name="index"></param>875 /// <returns></returns>876 private PSObject GetListItemPSObject(XmlNode xmlNode, bool ordered, ref int index)877 {878 if (xmlNode == null)879 return null;880 881 if (!xmlNode.LocalName.Equals("listItem", StringComparison.OrdinalIgnoreCase))882 return null;883 884 string text = string.Empty;885 886 if (xmlNode.ChildNodes.Count > 1)887 {888 WriteMamlInvalidChildNodeCountError(xmlNode, "para", 1);889 }890 891 foreach (XmlNode childNode in xmlNode.ChildNodes)892 {893 if (childNode.LocalName.Equals("para", StringComparison.OrdinalIgnoreCase))894 {895 text = childNode.InnerText.Trim();896 continue;897 }898 899 WriteMamlInvalidChildNodeError(xmlNode, childNode);900 }901 902 string tag = string.Empty;903 if (ordered)904 {905 tag = index.ToString("d2", CultureInfo.CurrentCulture);906 tag += ". ";907 index++;908 }909 else910 {911 tag = "* ";912 }913 914 PSObject mshObject = new PSObject();915 916 mshObject.Properties.Add(new PSNoteProperty("Text", text));917 mshObject.Properties.Add(new PSNoteProperty("Tag", tag));918 919 mshObject.TypeNames.Clear();920 if (ordered)921 {922 mshObject.TypeNames.Add("MamlOrderedListTextItem");923 }924 else925 {926 mshObject.TypeNames.Add("MamlUnorderedListTextItem");927 }928 929 mshObject.TypeNames.Add("MamlTextItem");930 931 return mshObject;932 }933 934 /// <summary>935 /// Convert definitionList node into an array of PSObject, an for936 /// each definitionListItem node inside this node.937 /// </summary>938 /// <param name="xmlNode"></param>939 /// <returns></returns>940 private ArrayList GetDefinitionListPSObjects(XmlNode xmlNode)941 {942 ArrayList mshObjects = new ArrayList();943 944 if (xmlNode == null)945 return mshObjects;946 947 if (!xmlNode.LocalName.Equals("definitionList", StringComparison.OrdinalIgnoreCase))948 return mshObjects;949 950 if (xmlNode.ChildNodes == null || xmlNode.ChildNodes.Count == 0)951 return mshObjects;952 953 foreach (XmlNode childNode in xmlNode.ChildNodes)954 {955 if (childNode.LocalName.Equals("definitionListItem", StringComparison.OrdinalIgnoreCase))956 {957 PSObject definitionListItemPSObject = GetDefinitionListItemPSObject(childNode);958 959 if (definitionListItemPSObject != null)960 mshObjects.Add(definitionListItemPSObject);961 962 continue;963 }964 965 // If we get here, we found some node that is not supported.966 WriteMamlInvalidChildNodeError(xmlNode, childNode);967 }968 969 return mshObjects;970 }971 972 /// <summary>973 /// Convert an definitionListItem node into an PSObject974 ///975 /// For example976 /// <definitionListItem>977 /// <term>978 /// term text979 /// </term>980 /// <definition>981 /// <para>982 /// definition text983 /// </para>984 /// </definition>985 /// </definitionListItem>986 /// In this case, an PSObject of type "definitionListText" will be created with following987 /// properties988 /// a. term="term text"989 /// b. definition="definition text"990 /// </summary>991 /// <param name="xmlNode"></param>992 /// <returns></returns>993 private PSObject GetDefinitionListItemPSObject(XmlNode xmlNode)994 {995 if (xmlNode == null)996 return null;997 998 if (!xmlNode.LocalName.Equals("definitionListItem", StringComparison.OrdinalIgnoreCase))999 return null;1000 1001 string term = null;1002 string definition = null;1003 1004 foreach (XmlNode childNode in xmlNode.ChildNodes)1005 {1006 if (childNode.LocalName.Equals("term", StringComparison.OrdinalIgnoreCase))1007 {1008 term = childNode.InnerText.Trim();1009 continue;1010 }1011 1012 if (childNode.LocalName.Equals("definition", StringComparison.OrdinalIgnoreCase))1013 {1014 definition = GetDefinitionText(childNode);1015 continue;1016 }1017 1018 // If we get here, we found some node that is not supported.1019 WriteMamlInvalidChildNodeError(xmlNode, childNode);1020 }1021 1022 if (string.IsNullOrEmpty(term))1023 return null;1024 1025 PSObject mshObject = new PSObject();1026 1027 mshObject.Properties.Add(new PSNoteProperty("Term", term));1028 mshObject.Properties.Add(new PSNoteProperty("Definition", definition));1029 1030 mshObject.TypeNames.Clear();1031 mshObject.TypeNames.Add("MamlDefinitionTextItem");1032 mshObject.TypeNames.Add("MamlTextItem");1033 1034 return mshObject;1035 }1036 1037 /// <summary>1038 /// Get the text for definition. The will treat some intermediate nodes like "definition" and "para"1039 /// </summary>1040 /// <param name="xmlNode"></param>1041 /// <returns></returns>1042 private string GetDefinitionText(XmlNode xmlNode)1043 {1044 if (xmlNode == null)1045 return null;1046 1047 if (!xmlNode.LocalName.Equals("definition", StringComparison.OrdinalIgnoreCase))1048 return null;1049 1050 if (xmlNode.ChildNodes == null || xmlNode.ChildNodes.Count == 0)1051 return string.Empty;1052 1053 if (xmlNode.ChildNodes.Count > 1)1054 {1055 WriteMamlInvalidChildNodeCountError(xmlNode, "para", 1);1056 }1057 1058 string text = string.Empty;1059 1060 foreach (XmlNode childNode in xmlNode.ChildNodes)1061 {1062 if (childNode.LocalName.Equals("para", StringComparison.OrdinalIgnoreCase))1063 {1064 text = childNode.InnerText.Trim();1065 continue;1066 }1067 1068 WriteMamlInvalidChildNodeError(xmlNode, childNode);1069 }1070 1071 return text;1072 }1073 1074 #endregion1075 1076 #region Preformatted string processing1077 1078 /// <summary>1079 /// This is for getting preformatted text from an xml document.1080 ///1081 /// Normally in xml document, preformatted text will be indented by1082 /// a fix amount based on its position. The task of this function1083 /// is to remove that fixed amount from the text.1084 ///1085 /// For example, in xml,1086 /// <preformatted>1087 /// void function()1088 /// {1089 /// // call some other function here;1090 /// }1091 /// </preformatted>1092 /// we can find that the preformatted text are indented unanimously1093 /// by 4 spaces because of its position in xml.1094 ///1095 /// After massaging in this function, the result text will be,1096 ///1097 /// void function1098 /// {1099 /// // call some other function here;1100 /// }1101 ///1102 /// please notice that the indention is reduced.1103 /// </summary>1104 /// <param name="text"></param>1105 /// <returns></returns>1106 private static string GetPreformattedText(string text)1107 {1108 // we are assuming tabsize=4 here.1109 // It is discouraged to use tab in preformatted text.1110 1111 string noTabText = text.Replace("\t", " ");1112 string[] lines = noTabText.Split('\n');1113 string[] trimedLines = TrimLines(lines);1114 1115 if (trimedLines == null || trimedLines.Length == 0)1116 return string.Empty;1117 1118 int minIndentation = GetMinIndentation(trimedLines);1119 1120 string[] shortedLines = new string[trimedLines.Length];1121 for (int i = 0; i < trimedLines.Length; i++)1122 {1123 if (IsEmptyLine(trimedLines[i]))1124 {1125 shortedLines[i] = trimedLines[i];1126 }1127 else1128 {1129 shortedLines[i] = trimedLines[i].Remove(0, minIndentation);1130 }1131 }1132 1133 StringBuilder result = new StringBuilder();1134 for (int i = 0; i < shortedLines.Length; i++)1135 {1136 result.AppendLine(shortedLines[i]);1137 }1138 1139 return result.ToString();1140 }1141 1142 /// <summary>1143 /// Trim empty lines from the either end of an string array.1144 /// </summary>1145 /// <param name="lines">Lines to trim.</param>1146 /// <returns>An string array with empty lines trimed on either end.</returns>1147 private static string[] TrimLines(string[] lines)1148 {1149 if (lines == null || lines.Length == 0)1150 return null;1151 1152 int i = 0;1153 for (i = 0; i < lines.Length; i++)1154 {1155 if (!IsEmptyLine(lines[i]))1156 break;1157 }1158 1159 int start = i;1160 1161 if (start == lines.Length)1162 return null;1163 1164 for (i = lines.Length - 1; i >= start; i--)1165 {1166 if (!IsEmptyLine(lines[i]))1167 break;1168 }1169 1170 int end = i;1171 1172 string[] result = new string[end - start + 1];1173 for (i = start; i <= end; i++)1174 {1175 result[i - start] = lines[i];1176 }1177 1178 return result;1179 }1180 1181 /// <summary>1182 /// Get minimum indentation of a paragraph.1183 /// </summary>1184 /// <param name="lines"></param>1185 /// <returns></returns>1186 private static int GetMinIndentation(string[] lines)1187 {1188 int minIndentation = -1;1189 1190 for (int i = 0; i < lines.Length; i++)1191 {1192 if (IsEmptyLine(lines[i]))1193 continue;1194 1195 int indentation = GetIndentation(lines[i]);1196 1197 if (minIndentation < 0 || indentation < minIndentation)1198 minIndentation = indentation;1199 }1200 