// Copyright © Microsoft Corporation. // This source file is subject to the Microsoft Permissive License. // See http://www.microsoft.com/resources/sharedsource/licensingbasics/sharedsourcelicenses.mspx. // All other rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using System.Xml; using System.Xml.XPath; namespace Microsoft.Ddue.Tools { /// /// The LiveExampleComponent replaces ddue:CodeReference elements with links to runnable code samples, /// if the type is "run" or "view". All other kinds of code samples are passed through to the /// standard example component, except that the type prefix is removed. A Parsnip "approval" file can /// be used to omit code samples that did not pass validation, or to replace broken samples with a message /// to that effect. /// public class LiveExampleComponent : BuildComponent { readonly string wdxNamespace = "http://temp.uri/wdx"; readonly string wdxPrefix = "wdx"; XPathExpression selector; XmlNamespaceManager context; bool omitBadExamples; //bool runBadExamples; Dictionary sampleInfoTable; public LiveExampleComponent(BuildAssembler assembler, XPathNavigator configuration) : base(assembler, configuration) { XPathNavigator parsnip_node = configuration.SelectSingleNode("parsnip"); string approvedFile = null; if (parsnip_node != null) { approvedFile = parsnip_node.GetAttribute("approved-file", String.Empty); string omitBadExamplesValue = parsnip_node.GetAttribute("omit-bad-examples", String.Empty); if (!string.IsNullOrEmpty(omitBadExamplesValue)) omitBadExamples = Boolean.Parse(omitBadExamplesValue); //string runBadExamplesValue = parsnip_node.GetAttribute("run-bad-examples", String.Empty); //if (!string.IsNullOrEmpty(runBadExamplesValue)) // runBadExamples = Boolean.Parse(runBadExamplesValue); } if (string.IsNullOrEmpty(approvedFile)) WriteMessage(MessageLevel.Warn, "No approved samples file specified; all available samples will be included."); else LoadApprovedFile(approvedFile); context = new CustomContext(); context.AddNamespace("ddue", "http://ddue.schemas.microsoft.com/authoring/2003/5"); selector = XPathExpression.Compile("//ddue:codeReference"); selector.SetContext(context); } static XPathNavigator[] ConvertIteratorToArray(XPathNodeIterator iterator) { XPathNavigator[] result = new XPathNavigator[iterator.Count]; for (int i = 0; i < result.Length; i++) { iterator.MoveNext(); result[i] = iterator.Current.Clone(); } return (result); } public override void Apply(XmlDocument document, string key) { XPathNodeIterator nodesIterator = document.CreateNavigator().Select(selector); XPathNavigator[] nodes = ConvertIteratorToArray(nodesIterator); foreach (XPathNavigator node in nodes) { CodeReference cref = new CodeReference(node.Value); SampleInfo si = null; if (sampleInfoTable != null && cref.ExampleName != null) sampleInfoTable.TryGetValue(cref.ExampleName, out si); WriteMessage(MessageLevel.Info, string.Format("*** codeReference={0}; approved={1}; type={2}", node.Value, (si == null) ? false : si.IsApproved("CS"), cref.Type)); switch (cref.Type) { case CodeReferenceType.Msdn: // TODO: remove "msdn:" from code reference and let ExampleComponent handle the snippet. // We'll either pass this through to the regular ExampleComponent or delete the node. WriteMessage(MessageLevel.Warn, "MSDN-only links not implemented yet."); break; case CodeReferenceType.Run: case CodeReferenceType.View: if (si != null || !omitBadExamples) { WriteMessage(MessageLevel.Info, string.Format("+ LiveCode: Kind={0}, SampleName={1}", cref.Type.ToString(), cref.ExamplePath)); XmlWriter writer = node.InsertAfter(); writer.WriteStartElement(wdxPrefix, "LiveCode", wdxNamespace); writer.WriteAttributeString("Kind", cref.Type.ToString()); writer.WriteAttributeString("SampleName", cref.ExamplePath); writer.WriteAttributeString("runat", "server"); writer.WriteEndElement(); writer.Close(); node.DeleteSelf(); } break; case CodeReferenceType.Snippet: // Ignore; let ExampleComponent handle the snippet. break; default: WriteMessage(MessageLevel.Warn, string.Format("Invalid code example reference ignored: '{0}'", node.Value)); break; } } } /// /// LoadApprovedFile reads the Parsnip approval file into a memory structure for easy lookup. We're assuming that the /// content of this file is well formed and valid. Semantic errors will be silently ignored. Only samples with /// approved units will be added to the lookup table. /// void LoadApprovedFile(string pathName) { WriteMessage(MessageLevel.Info, string.Format("Loading Parsnip 'Approved' file: {0}", pathName)); sampleInfoTable = new Dictionary(); using (XmlReader reader = XmlReader.Create(pathName)) { SampleInfo si = null; string sample_name = null; while (reader.Read()) { switch (reader.NodeType) { case XmlNodeType.Element: if (reader.Name == "Sample") { sample_name = reader.GetAttribute("name"); if (!string.IsNullOrEmpty(sample_name)) si = new SampleInfo(sample_name); } else if (si != null && reader.Name == "Unit") { if (reader.GetAttribute("include") == "true") si.AddApprovedUnit(reader.GetAttribute("name")); } break; case XmlNodeType.EndElement: if (reader.Name == "Sample") { if (si != null) { try { if (si.ApprovedUnitsCount > 0) sampleInfoTable.Add(si.Name, si); } catch (Exception x) { WriteMessage(MessageLevel.Warn, string.Format("Sample {0} cannot be loaded {1}", si.Name, x.Message)); } } si = null; } break; default: break; } } } } } internal class SampleInfo { string _name; internal string Name { get { return _name; } } List _approvedUnits; internal SampleInfo(string name) { _name = name; } internal void AddApprovedUnit(string unit_name) { if (_approvedUnits == null) _approvedUnits = new List(); _approvedUnits.Add(unit_name); } internal bool IsApproved(string unit_name) { return (_approvedUnits == null) ? false : _approvedUnits.Contains(unit_name); } internal int ApprovedUnitsCount { get { return (_approvedUnits == null) ? 0 : _approvedUnits.Count; } } } }