/// <inheritdoc /> public override void Apply(XmlDocument document, string key) { // Set the context CustomContext context = new CustomContext(contextNamespaces); context["key"] = key; // Evaluate the condition XPathExpression xPathLocal = xPath.Clone(); xPathLocal.SetContext(context); object result = document.CreateNavigator().Evaluate(xPathLocal); // Try to interpret the result as a node set XPathNodeIterator resultNodeIterator = result as XPathNodeIterator; if(resultNodeIterator != null) { // If it is, apply the child components to each node value foreach(XPathNavigator resultNode in resultNodeIterator.ToArray()) ApplyComponents(document, resultNode.Value); } else { // If it isn't, apply the child components to the string value of the result ApplyComponents(document, result.ToString()); } }
/// <inheritdoc /> public override void Apply(XmlDocument document, string key) { // Set the key in the XPath context var context = new CustomContext(); context["key"] = key; // Perform each copy command foreach(CopyFromFilesCommand copyCommand in copyCommands) copyCommand.Apply(document, context); }
//===================================================================== /// <summary> /// This is used to write the document to its destination file /// </summary> private void WriteDocuments() { foreach(var kv in documentList.GetConsumingEnumerable()) { var document = kv.Value; // Set the evaluation context CustomContext context = new CustomContext(); context["key"] = kv.Key; XPathExpression xpath = pathExpression.Clone(); xpath.SetContext(context); // Evaluate the path string path = document.CreateNavigator().Evaluate(xpath).ToString(); if(basePath != null) path = Path.Combine(basePath, path); string targetDirectory = Path.GetDirectoryName(path); if(!Directory.Exists(targetDirectory)) Directory.CreateDirectory(targetDirectory); if(writeXhtmlNamespace) { document.DocumentElement.SetAttribute("xmlns", "http://www.w3.org/1999/xhtml"); document.LoadXml(document.OuterXml); } // Save the document try { // selectExpression determines which nodes get saved. If there is no selectExpression we simply // save the root node as before. If there is a selectExpression, we evaluate the XPath expression // and save the resulting node set. The select expression also enables the "literal-text" processing // instruction, which outputs its content as unescaped text. if(selectExpression == null) { using(XmlWriter writer = XmlWriter.Create(path, settings)) { document.Save(writer); } } else { // IMPLEMENTATION NOTE: The separate StreamWriter is used to maintain XML indenting. Without it // the XmlWriter won't honor our indent settings after plain text nodes have been written. using(StreamWriter output = File.CreateText(path)) { using(XmlWriter writer = XmlWriter.Create(output, settings)) { XPathExpression selectXPath = selectExpression.Clone(); selectXPath.SetContext(context); XPathNodeIterator ni = document.CreateNavigator().Select(selectExpression); while(ni.MoveNext()) { if(ni.Current.NodeType == XPathNodeType.ProcessingInstruction && ni.Current.Name.Equals("literal-text")) { writer.Flush(); output.Write(ni.Current.Value); } else ni.Current.WriteSubtree(writer); } } } } // Raise an event to indicate that a file was created this.OnComponentEvent(new FileCreatedEventArgs(path, true)); } catch(IOException e) { this.WriteMessage(kv.Key, MessageLevel.Error, "An access error occurred while attempting to " + "save to the file '{0}'. The error message is '{1}'", path, e.GetExceptionMessage()); } catch(XmlException e) { this.WriteMessage(kv.Key, MessageLevel.Error, "Invalid XML was written to the output " + "file '{0}'. The error message is '{1}'", path, e.GetExceptionMessage()); } } }
//===================================================================== /// <inheritdoc /> public override void Initialize(XPathNavigator configuration) { MessageLevel level; bool isAttribute, ignoreCase; // Set up the context XPathNodeIterator contextNodes = configuration.Select("context"); foreach(XPathNavigator contextNode in contextNodes) contextNamespaces[contextNode.GetAttribute("prefix", String.Empty)] = contextNode.GetAttribute("name", String.Empty); // This is only used by the indices and won't change this.Context = new CustomContext(contextNamespaces); // Set up the indices XPathNodeIterator indexNodes = configuration.Select("index"); foreach(XPathNavigator indexNode in indexNodes) { // Create the index IndexedCache index = this.CreateIndex(indexNode); #if DEBUG this.WriteMessage(MessageLevel.Diagnostic, "Loading {0} index", index.Name); DateTime startLoad = DateTime.Now; #endif // Search the data directories for entries XPathNodeIterator dataNodes = indexNode.Select("data"); foreach(XPathNavigator dataNode in dataNodes) index.AddDocuments(dataNode); // Getting the count from a database cache can be expensive so only report it if it will be seen if(this.BuildAssembler.VerbosityLevel == MessageLevel.Info) this.WriteMessage(MessageLevel.Info, "Indexed {0} elements", index.Count); #if DEBUG TimeSpan loadTime = (DateTime.Now - startLoad); this.WriteMessage(MessageLevel.Diagnostic, "Load time: {0} seconds", loadTime.TotalSeconds); #endif BuildComponentCore.Data.Add(index.Name, index); } // Get the copy commands XPathNodeIterator copyNodes = configuration.Select("copy"); foreach(XPathNavigator copyNode in copyNodes) { string sourceName = copyNode.GetAttribute("name", String.Empty); if(String.IsNullOrWhiteSpace(sourceName)) this.WriteMessage(MessageLevel.Error, "Each copy command must specify an index to copy from"); string keyXPath = copyNode.GetAttribute("key", String.Empty); if(String.IsNullOrWhiteSpace(keyXPath)) keyXPath = "string($key)"; string sourceXPath = copyNode.GetAttribute("source", String.Empty); if(String.IsNullOrWhiteSpace(sourceXPath)) this.WriteMessage(MessageLevel.Error, "When instantiating a CopyFromIndex component, you " + "must specify a source XPath format using the source attribute"); string targetXPath = copyNode.GetAttribute("target", String.Empty); if(String.IsNullOrWhiteSpace(targetXPath)) this.WriteMessage(MessageLevel.Error, "When instantiating a CopyFromIndex component, you " + "must specify a target XPath format using the target attribute"); isAttribute = ignoreCase = false; string boolValue = copyNode.GetAttribute("attribute", String.Empty); if(!String.IsNullOrWhiteSpace(boolValue) && !Boolean.TryParse(boolValue, out isAttribute)) this.WriteMessage(MessageLevel.Error, "The 'attribute' attribute value is not a valid Boolean"); boolValue = copyNode.GetAttribute("ignoreCase", String.Empty); if(!String.IsNullOrWhiteSpace(boolValue) && !Boolean.TryParse(boolValue, out ignoreCase)) this.WriteMessage(MessageLevel.Error, "The ignoreCase attribute value is not a valid Boolean"); IndexedCache index = (IndexedCache)BuildComponentCore.Data[sourceName]; CopyFromIndexCommand copyCommand = new CopyFromIndexCommand(this, index, keyXPath, sourceXPath, targetXPath, isAttribute, ignoreCase); string messageLevel = copyNode.GetAttribute("missing-entry", String.Empty); if(!String.IsNullOrWhiteSpace(messageLevel)) if(Enum.TryParse<MessageLevel>(messageLevel, true, out level)) copyCommand.MissingEntry = level; else this.WriteMessage(MessageLevel.Error, "'{0}' is not a message level.", messageLevel); messageLevel = copyNode.GetAttribute("missing-source", String.Empty); if(!String.IsNullOrWhiteSpace(messageLevel)) if(Enum.TryParse<MessageLevel>(messageLevel, true, out level)) copyCommand.MissingSource = level; else this.WriteMessage(MessageLevel.Error, "'{0}' is not a message level.", messageLevel); messageLevel = copyNode.GetAttribute("missing-target", String.Empty); if(!String.IsNullOrWhiteSpace(messageLevel)) if(Enum.TryParse<MessageLevel>(messageLevel, true, out level)) copyCommand.MissingTarget = level; else this.WriteMessage(MessageLevel.Error, "'{0}' is not a message level.", messageLevel); copyCommands.Add(copyCommand); } XPathNodeIterator componentNodes = configuration.Select("components/component"); foreach(XPathNavigator componentNode in componentNodes) { // Get the ID of the copy component string id = componentNode.GetAttribute("id", String.Empty); if(String.IsNullOrWhiteSpace(id)) this.WriteMessage(MessageLevel.Error, "Each copy component element must have an id attribute"); var copyComponentFactory = copyComponentFactories.FirstOrDefault(g => g.Metadata.Id == id); if(copyComponentFactory == null) this.WriteMessage(MessageLevel.Error, "A copy component with the ID '{0}' could not be found", id); try { var copyComponent = copyComponentFactory.Value.Create(this); copyComponent.Initialize(componentNode.Clone(), BuildComponentCore.Data); components.Add(copyComponent); } catch(Exception ex) { this.WriteMessage(MessageLevel.Error, "An error occurred while attempting to instantiate " + "the '{0}' copy component. The error message is: {1}{2}", id, ex.Message, ex.InnerException != null ? "\r\n" + ex.InnerException.Message : String.Empty); } } if(components.Count != 0) this.WriteMessage(MessageLevel.Info, "Loaded {0} copy components", components.Count); }
/// <inheritdoc /> public override void Apply(XmlDocument document, string key) { // Set the key in the XPath context var context = new CustomContext(contextNamespaces); context["key"] = key; // Perform each copy command foreach(CopyFromIndexCommand copyCommand in copyCommands) copyCommand.Apply(document, context); // Apply changes for each sub-component, if any foreach(CopyComponentCore component in components) component.Apply(document, key); }
/// <inheritdoc /> public override void Apply(XmlDocument document, string key) { // Set up the test CustomContext context = new CustomContext(contextNamespaces); context["key"] = key; XPathExpression test = condition.Clone(); test.SetContext(context); // Evaluate the condition bool result = (bool)document.CreateNavigator().Evaluate(test); // On the basis of the condition, execute either the true or the false branch if(result) { foreach(BuildComponentCore component in trueBranch) component.Apply(document, key); } else { foreach(BuildComponentCore component in falseBranch) component.Apply(document, key); } }
//===================================================================== /// <inheritdoc /> public override void Initialize(XPathNavigator configuration) { // Get the context. This will contain namespaces that prefix the elements to find. var context = new CustomContext(); XPathNodeIterator contextNodes = configuration.Select("context"); foreach(XPathNavigator cn in contextNodes) context.AddNamespace(cn.GetAttribute("prefix", String.Empty), cn.GetAttribute("name", String.Empty)); // Item keys are compared case-insensitively content = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); elements = new List<SharedContentElement>(); // Get the elements to be resolved XPathNodeIterator resolve_nodes = configuration.Select("replace"); foreach(XPathNavigator resolve_node in resolve_nodes) { // Get the XPath expression used to find the elements to replace string path = resolve_node.GetAttribute("elements", String.Empty); // If not defined, assume include and includeAttribute are to be replaced if(String.IsNullOrEmpty(path)) path = "//include | //includeAttribute"; try { XPathExpression path_expresion = XPathExpression.Compile(path, context); } catch(XPathException) { this.WriteMessage(MessageLevel.Error, "The elements expression '{0}' is not a valid XPath", path); } // Get the XPath expression used to get the item name to insert string item = resolve_node.GetAttribute("item", String.Empty); if(String.IsNullOrEmpty(item)) item = "string(@item)"; try { XPathExpression item_expression = XPathExpression.Compile(item, context); } catch(XPathException) { this.WriteMessage(MessageLevel.Error, "The item expression '{0}' is not a valid XPath", item); } // Get the XPath expression used to find parameter elements string parameters = resolve_node.GetAttribute("parameters", String.Empty); if(String.IsNullOrEmpty(parameters)) parameters = "parameter"; // Get the XPath expression used to get the attribute name for attribute items string attribute = resolve_node.GetAttribute("attribute", String.Empty); if(String.IsNullOrEmpty(attribute)) attribute = "string(@name)"; elements.Add(new SharedContentElement(path, item, parameters, attribute, context)); } // If not defined, assume include and includeAttribute are to be replaced using the default names if(elements.Count == 0) elements.Add(new SharedContentElement("//include | //includeAttribute", "string(@item)", "parameter", "string(@name)", context)); // Load the content item files XPathNodeIterator content_nodes = configuration.Select("content"); foreach(XPathNavigator content_node in content_nodes) { string sharedContentFiles = content_node.GetAttribute("file", String.Empty); if(String.IsNullOrEmpty(sharedContentFiles)) this.WriteMessage(MessageLevel.Error, "The content/@file attribute must specify a path."); this.ParseDocuments(sharedContentFiles); } this.WriteMessage(MessageLevel.Info, "Loaded {0} shared content items.", content.Count); }
/// <summary> /// This is used to get the string result from evaluating an XPath expression against the given document /// and a context created from a set of key/value pairs. /// </summary> /// <param name="document">The document to use</param> /// <param name="expression">The XPath expression to evaluate</param> /// <param name="keyValuePairs">A set of key/value pairs to use when creating the context</param> /// <returns>The evaluated expression result</returns> /// <example> /// <code language="cs"> /// string result = document.EvalXPathExpr("concat($key, '.htm')", "key", "filename"); /// </code> /// </example> /// <exception cref="ArgumentException">This is thrown if the <paramref name="keyValuePairs"/> /// parameter contains an odd number of parameters.</exception> public static string EvalXPathExpr(this IXPathNavigable document, XPathExpression expression, params string[] keyValuePairs) { if(keyValuePairs.Length % 2 != 0) throw new ArgumentException("There must be a value for every key name specified", "keyValuePairs"); CustomContext cc = new CustomContext(); for(int i = 0; i < keyValuePairs.Length; i += 2) cc[keyValuePairs[i]] = keyValuePairs[i + 1]; return document.EvalXPathExpr(expression, cc); }
/// <summary> /// This is used to get the string result from evaluating an XPath expression against the given /// document and context. /// </summary> /// <param name="document">The document to use</param> /// <param name="expression">The XPath expression to evaluate</param> /// <param name="context">The context to use</param> /// <returns>The evaluated expression result</returns> /// <overloads>There are two overloads for this method</overloads> public static string EvalXPathExpr(this IXPathNavigable document, XPathExpression expression, CustomContext context) { XPathExpression t = expression.Clone(); t.SetContext(context); return document.CreateNavigator().Evaluate(t).ToString(); }
//===================================================================== /// <inheritdoc /> /// <remarks>See the <see cref="CodeBlockComponent"/> class topic for an example of the configuration and /// usage.</remarks> /// <exception cref="ConfigurationErrorsException">This is thrown if an error is detected in the /// configuration.</exception> public override void Initialize(XPathNavigator configuration) { XPathNavigator nav; string value = null, syntaxFile, styleFile; bool allowMissingSource = false, useDefaultTitle = false; int defaultTabSize = 8; outputPaths = new List<string>(); topicCodeBlocks = new Dictionary<string, Dictionary<string, XmlNode>>(); Assembly asm = Assembly.GetExecutingAssembly(); FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(asm.Location); base.WriteMessage(MessageLevel.Info, "[{0}, version {1}]\r\n Code Block Component. " + "{2}.\r\n Portions copyright (c) 2003-2015, Jonathan de Halleux, All rights reserved.\r\n" + " https://GitHub.com/EWSoftware/SHFB", fvi.ProductName, fvi.ProductVersion, fvi.LegalCopyright); // The <basePath> element is optional. If not set, it will assume the current folder as the base // path for source references with relative paths. nav = configuration.SelectSingleNode("basePath"); if(nav != null) basePath = nav.GetAttribute("value", String.Empty); if(String.IsNullOrEmpty(basePath)) basePath = Directory.GetCurrentDirectory(); if(basePath[basePath.Length - 1] != '\\') basePath += @"\"; // Get the output paths foreach(XPathNavigator path in configuration.Select("outputPaths/path")) { value = path.GetAttribute("value", String.Empty); if(value[value.Length - 1] != '\\') value += @"\"; if(!Directory.Exists(value)) Directory.CreateDirectory(value); outputPaths.Add(value); // The Open XML format doesn't support all features and requires a custom transformation if(value.IndexOf("OpenXML", StringComparison.OrdinalIgnoreCase) != -1) isOpenXml = true; // The Markdown format doesn't support any features if(value.IndexOf("Markdown", StringComparison.OrdinalIgnoreCase) != -1) isMarkdown = true; } if(outputPaths.Count == 0) throw new ConfigurationErrorsException("You must specify at least one <path> element in the " + "<outputPaths> element. You may need to delete and re-add the component to the project " + "to obtain updated configuration settings."); // The <allowMissingSource> element is optional. If not set, missing source files generate an error. nav = configuration.SelectSingleNode("allowMissingSource"); if(nav != null) { value = nav.GetAttribute("value", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out allowMissingSource)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "<allowMissingSource> 'value' attribute."); } if(!allowMissingSource) messageLevel = MessageLevel.Error; else messageLevel = MessageLevel.Warn; // The <removeRegionMarkers> element is optional. If not set, region markers in imported code are // left alone. nav = configuration.SelectSingleNode("removeRegionMarkers"); if(nav != null) { value = nav.GetAttribute("value", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out removeRegionMarkers)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "<removeRegionMarkers> 'value' attribute."); } // The <colorizer> element is required and defines the defaults for the code colorizer nav = configuration.SelectSingleNode("colorizer"); if(nav == null) throw new ConfigurationErrorsException("You must specify a <colorizer> element to define " + "the code colorizer options."); // The file and URL values are all required syntaxFile = nav.GetAttribute("syntaxFile", String.Empty); styleFile = nav.GetAttribute("styleFile", String.Empty); stylesheet = nav.GetAttribute("stylesheet", String.Empty); scriptFile = nav.GetAttribute("scriptFile", String.Empty); if(String.IsNullOrEmpty(syntaxFile)) throw new ConfigurationErrorsException("You must specify a 'syntaxFile' attribute on the " + "<colorizer> element."); if(String.IsNullOrEmpty(styleFile)) throw new ConfigurationErrorsException("You must specify a 'styleFile' attribute on the " + "<colorizer> element."); if(String.IsNullOrEmpty(stylesheet) && !isOpenXml && !isMarkdown) throw new ConfigurationErrorsException("You must specify a 'stylesheet' attribute on the " + "<colorizer> element"); if(String.IsNullOrEmpty(scriptFile) && !isOpenXml && !isMarkdown) throw new ConfigurationErrorsException("You must specify a 'scriptFile' attribute on the " + "<colorizer> element"); // The syntax and style files must also exist. The "copy" image URL is just a location and it // doesn't have to exist yet. syntaxFile = Path.GetFullPath(syntaxFile); styleFile = Path.GetFullPath(styleFile); if(!File.Exists(syntaxFile)) throw new ConfigurationErrorsException("The specified syntax file could not be found: " + syntaxFile); if(!File.Exists(styleFile)) throw new ConfigurationErrorsException("The specified style file could not be found: " + styleFile); if(!isOpenXml && !isMarkdown) { stylesheet = Path.GetFullPath(stylesheet); scriptFile = Path.GetFullPath(scriptFile); if(!File.Exists(stylesheet)) throw new ConfigurationErrorsException("Could not find style sheet file: " + stylesheet); if(!File.Exists(scriptFile)) throw new ConfigurationErrorsException("Could not find script file: " + scriptFile); } // Optional attributes defaultLanguage = nav.GetAttribute("language", String.Empty); if(String.IsNullOrEmpty(defaultLanguage)) defaultLanguage = "none"; value = nav.GetAttribute("numberLines", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out numberLines)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "'numberLines' attribute."); value = nav.GetAttribute("outlining", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out outliningEnabled)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "'outlining' attribute."); value = nav.GetAttribute("keepSeeTags", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out keepSeeTags)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "'keepSeeTags' attribute."); value = nav.GetAttribute("tabSize", String.Empty); if(!String.IsNullOrEmpty(value) && !Int32.TryParse(value, out defaultTabSize)) throw new ConfigurationErrorsException("You must specify an integer value for the 'tabSize' " + "attribute."); value = nav.GetAttribute("defaultTitle", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out useDefaultTitle)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "'defaultTitle' attribute."); if(!isMarkdown) { value = nav.GetAttribute("disabled", String.Empty); if(!String.IsNullOrEmpty(value) && !Boolean.TryParse(value, out isDisabled)) throw new ConfigurationErrorsException("You must specify a Boolean value for the " + "'disabled' attribute."); } else isDisabled = true; // Markdown doesn't support anything so it is always disabled if(isOpenXml) { numberLines = outliningEnabled = false; // If the default transform is specified, switch to the Open XML version. This can happen if // the user adds the code block component to their project to override the default settings. string defaultTransform = Path.Combine(Path.GetDirectoryName(asm.Location), @"PresentationStyles\Colorizer\highlight.xsl"); if(styleFile.Equals(defaultTransform, StringComparison.OrdinalIgnoreCase)) { styleFile = Path.Combine(Path.GetDirectoryName(defaultTransform), "highlight_openxml.xsl"); if(!File.Exists(styleFile)) throw new ConfigurationErrorsException("The specified style file could not be found: " + styleFile); } } // Initialize the code colorizer colorizer = new CodeColorizer(syntaxFile, styleFile); colorizer.UseDefaultTitle = useDefaultTitle; colorizer.TabSize = defaultTabSize; // Create the XPath queries var context = new CustomContext(); context.AddNamespace("ddue", "http://ddue.schemas.microsoft.com/authoring/2003/5"); referenceRoot = XPathExpression.Compile("document/comments"); referenceCode = XPathExpression.Compile("//code"); nestedRefCode = XPathExpression.Compile("code"); conceptualRoot = XPathExpression.Compile("document/topic"); conceptualCode = XPathExpression.Compile("//ddue:code"); conceptualCode.SetContext(context); nestedConceptCode = XPathExpression.Compile("ddue:code"); nestedConceptCode.SetContext(context); // Hook up the event handler to complete the process after the topic is transformed to HTML base.BuildAssembler.ComponentEvent += TransformComponent_TopicTransformed; }
//===================================================================== /// <inheritdoc /> public override void Initialize(XPathNavigator configuration) { XPathNodeIterator contentNodes = configuration.Select("examples"); foreach(XPathNavigator contentNode in contentNodes) { string file = contentNode.GetAttribute("file", String.Empty); file = Environment.ExpandEnvironmentVariables(file); if(String.IsNullOrEmpty(file)) WriteMessage(MessageLevel.Error, "Each examples element must contain a file attribute."); LoadContent(file); } WriteMessage(MessageLevel.Info, "Loaded {0} code snippets", snippets.Count); XPathNodeIterator colorsNodes = configuration.Select("colors"); foreach(XPathNavigator colorsNode in colorsNodes) { string language = colorsNode.GetAttribute("language", String.Empty); List<ColorizationRule> rules = new List<ColorizationRule>(); XPathNodeIterator colorNodes = colorsNode.Select("color"); foreach(XPathNavigator colorNode in colorNodes) { string pattern = colorNode.GetAttribute("pattern", String.Empty); string region = colorNode.GetAttribute("region", String.Empty); string name = colorNode.GetAttribute("class", String.Empty); if(String.IsNullOrEmpty(region)) rules.Add(new ColorizationRule(pattern, name)); else rules.Add(new ColorizationRule(pattern, region, name)); } colorization[language] = rules; WriteMessage(MessageLevel.Info, "Loaded {0} colorization rules for the language '{1}'.", rules.Count, language); } CustomContext context = new CustomContext(); context.AddNamespace("ddue", "http://ddue.schemas.microsoft.com/authoring/2003/5"); selector = XPathExpression.Compile("//ddue:codeReference"); selector.SetContext(context); }
//===================================================================== /// <inheritdoc /> public override void Initialize(XPathNavigator configuration) { XPathNavigator syntaxNode = configuration.SelectSingleNode("syntax"); string syntaxInputXPath = syntaxNode.GetAttribute("input", String.Empty); if(String.IsNullOrEmpty(syntaxInputXPath)) throw new ConfigurationErrorsException("You must specify an XPath for input in the syntax element"); syntaxInput = XPathExpression.Compile(syntaxInputXPath); string syntaxOutputXPath = syntaxNode.GetAttribute("output", String.Empty); if(String.IsNullOrEmpty(syntaxOutputXPath)) throw new ConfigurationErrorsException("You must specify an XPath for output in the syntax element"); syntaxOutput = XPathExpression.Compile(syntaxOutputXPath); string attrValue = syntaxNode.GetAttribute("renderReferenceLinks", String.Empty); if(String.IsNullOrWhiteSpace(attrValue) || !Boolean.TryParse(attrValue, out renderReferenceLinks)) renderReferenceLinks = false; XPathNodeIterator generatorNodes = configuration.Select("generators/generator"); // Configuration changes are stored separately since the actual generators may be added to the // configuration file at build time. Substitution of the edited configuration is easier to do here. var generatorConfigs = configuration.SelectSingleNode("configurations"); // If we have configuration nodes, note the order of the syntax generators. These will be used to // order the snippets. if(generatorConfigs != null) { int order = 1; foreach(XPathNavigator id in generatorConfigs.Select("generator/@id")) languageOrder.Add(id.Value, order++); } foreach(XPathNavigator generatorNode in generatorNodes) { // Get the ID of the syntax generator string id = generatorNode.GetAttribute("id", String.Empty); if(String.IsNullOrWhiteSpace(id)) this.WriteMessage(MessageLevel.Error, "Each generator element must have an id attribute"); var generatorFactory = generatorFactories.FirstOrDefault(g => g.Metadata.Id == id); if(generatorFactory == null) this.WriteMessage(MessageLevel.Error, "A syntax generator with the ID '{0}' could not be found", id); // Track the languages for grouping generatorLanguages.Add(generatorFactory.Metadata.Id); languageSet.Add(generatorFactory.Metadata); foreach(string alternateId in (generatorFactory.Metadata.AlternateIds ?? String.Empty).Split( new[] { ',', ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)) generatorLanguages.Add(alternateId); try { var generator = generatorFactory.Value.Create(); var configNode = generatorNode.Clone(); if(generatorConfigs != null) { var alternateConfig = generatorConfigs.SelectSingleNode("generator[@id='" + id + "']"); if(alternateConfig != null && alternateConfig.HasChildren) { // Since there may be custom attributes on the generator node, we'll make a copy and // substitute the child elements that make up the configuration. var alternate = XElement.Parse(alternateConfig.OuterXml); var genNode = XElement.Parse(configNode.OuterXml); genNode.RemoveNodes(); genNode.Add(alternate.Elements()); configNode = genNode.CreateNavigator(); } } generator.Initialize(configNode); generators.Add(generator); } catch(Exception ex) { this.WriteMessage(MessageLevel.Error, "An error occurred while attempting to instantiate " + "the '{0}' syntax generator. The error message is: {1}{2}", id, ex.Message, ex.InnerException != null ? "\r\n" + ex.InnerException.Message : String.Empty); } } this.WriteMessage(MessageLevel.Info, "Loaded {0} syntax generators.", generators.Count); // If this is not found or set, we'll assume the presentation style does not support grouping var containerElement = configuration.SelectSingleNode("containerElement"); if(containerElement != null) { // If grouping is disabled, skip the remainder of the set up. This will happen if the user adds // a custom configuration to a project for a presentation style that doesn't support it. bool groupingEnabled; containerElementName = containerElement.GetAttribute("name", String.Empty); attrValue = containerElement.GetAttribute("groupingEnabled", String.Empty); if(String.IsNullOrWhiteSpace(attrValue) || !Boolean.TryParse(attrValue, out groupingEnabled)) groupingEnabled = false; if(!groupingEnabled || String.IsNullOrWhiteSpace(containerElementName)) return; // Get the "no example tab" options attrValue = containerElement.GetAttribute("addNoExampleTabs", String.Empty); if(String.IsNullOrWhiteSpace(attrValue) || !Boolean.TryParse(attrValue, out addNoExampleTabs)) addNoExampleTabs = true; attrValue = containerElement.GetAttribute("includeOnSingleSnippets", String.Empty); if(String.IsNullOrWhiteSpace(attrValue) || !Boolean.TryParse(attrValue, out includeOnSingleSnippets)) includeOnSingleSnippets = false; // Create the XPath queries used for code snippet grouping and sorting var context = new CustomContext(); context.AddNamespace("ddue", "http://ddue.schemas.microsoft.com/authoring/2003/5"); referenceRoot = XPathExpression.Compile("document/comments|document/syntax"); referenceCode = XPathExpression.Compile("//code|//div[@codeLanguage]"); conceptualRoot = XPathExpression.Compile("document/topic"); conceptualCode = XPathExpression.Compile("//ddue:code|//ddue:snippet"); conceptualCode.SetContext(context); // Hook up the event handler to group and sort code snippets just prior to XSL transformation this.BuildAssembler.ComponentEvent += TransformComponent_TopicTransforming; } }