summaryrefslogtreecommitdiffstats
path: root/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ExampleComponent.cs
diff options
context:
space:
mode:
Diffstat (limited to 'tools/Sandcastle/Source/BuildAssembler/BuildComponents/ExampleComponent.cs')
-rw-r--r--tools/Sandcastle/Source/BuildAssembler/BuildComponents/ExampleComponent.cs549
1 files changed, 549 insertions, 0 deletions
diff --git a/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ExampleComponent.cs b/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ExampleComponent.cs
new file mode 100644
index 0000000..d0d52d9
--- /dev/null
+++ b/tools/Sandcastle/Source/BuildAssembler/BuildComponents/ExampleComponent.cs
@@ -0,0 +1,549 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+using System;
+using System.Configuration;
+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 {
+
+ // a component to replace code references with snippets from a file
+
+ public class ExampleComponent : BuildComponent {
+
+ // instantiation logic
+
+ public ExampleComponent(BuildAssembler assembler, XPathNavigator configuration) : base(assembler, 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, String.Format("Each examples element must contain a file attribute."));
+ LoadContent(file);
+ }
+
+ WriteMessage(MessageLevel.Info, String.Format("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, String.Format("Loaded {0} colorization rules for the language '{1}'.", rules.Count, language));
+ }
+
+ context.AddNamespace("ddue", "http://ddue.schemas.microsoft.com/authoring/2003/5");
+
+ selector = XPathExpression.Compile("//ddue:codeReference");
+ selector.SetContext(context);
+ }
+
+ // snippet loading logic
+
+ private void LoadContent(string file) {
+
+ SnippetIdentifier key = new SnippetIdentifier();
+ string language;
+
+ WriteMessage(MessageLevel.Info, String.Format("Loading code snippet file '{0}'.", file));
+ try {
+ XmlReaderSettings settings = new XmlReaderSettings();
+ settings.CheckCharacters = false;
+ XmlReader reader = XmlReader.Create(file, settings);
+
+ try {
+ reader.MoveToContent();
+ while (!reader.EOF) {
+ if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "item")) {
+ key = new SnippetIdentifier(reader.GetAttribute("id"));
+ reader.Read();
+ } else if ((reader.NodeType == XmlNodeType.Element) && (reader.Name == "sampleCode")) {
+ language = reader.GetAttribute("language");
+
+ string content = reader.ReadString();
+
+ // If the element is empty, ReadString does not advance the reader, so we must do it manually
+ if (String.IsNullOrEmpty(content)) reader.Read();
+
+ if (!IsLegalXmlText(content)) {
+ Console.WriteLine("Snippet '{0}' language '{1}' contains illegal characters.", key, language);
+ throw new InvalidOperationException();
+ }
+
+ content = StripLeadingSpaces(content);
+
+ StoredSnippet snippet = new StoredSnippet(content, language);
+ List<StoredSnippet> values;
+ if (!snippets.TryGetValue(key, out values)) {
+ values = new List<StoredSnippet>();
+ snippets.Add(key, values);
+ }
+ values.Add(snippet);
+ } else {
+ reader.Read();
+ }
+ }
+ } catch (XmlException e) {
+ WriteMessage(MessageLevel.Warn, String.Format("The contents of the snippet file '{0}' are not well-formed XML. The error message is: {1}. Some snippets may be lost.", file, e.Message));
+ } finally {
+ reader.Close();
+ }
+
+ } catch (IOException e) {
+ WriteMessage(MessageLevel.Error, String.Format("An access error occured while attempting to read the snippet file '{0}'. The error message is: {1}", file, e.Message));
+ }
+
+ }
+
+ // logic for checking XML
+
+ private bool IsLegalXmlCharacter (char c) {
+ if (c < 0x20) {
+ return ( (c == 0x09) || (c == 0x0A) || (c == 0x0D) );
+ } else {
+ if (c < 0xD800) {
+ return(true);
+ } else {
+ return( (c >= 0xE000) && (c <= 0xFFFD) );
+ }
+ }
+ }
+
+ private bool IsLegalXmlText (string text) {
+ foreach (char c in text) {
+ if (!IsLegalXmlCharacter(c)) return (false);
+ }
+ return (true);
+ }
+
+ // the snippet store
+
+ private Dictionary<SnippetIdentifier,List<StoredSnippet>> snippets = new Dictionary<SnippetIdentifier,List<StoredSnippet>>();
+
+ // the actual work of the component
+
+ public override void Apply(XmlDocument document, string key) {
+ XPathNodeIterator nodesIterator = document.CreateNavigator().Select(selector);
+ XPathNavigator[] nodes = BuildComponentUtilities.ConvertNodeIteratorToArray(nodesIterator);
+ foreach (XPathNavigator node in nodes) {
+
+ string reference = node.Value;
+
+ // check for validity of reference
+ if (validSnippetReference.IsMatch(reference)) {
+
+
+ SnippetIdentifier[] identifiers = SnippetIdentifier.ParseReference(reference);
+
+ if (identifiers.Length == 1) {
+ // one snippet referenced
+
+ SnippetIdentifier identifier = identifiers[0];
+ List<StoredSnippet> values;
+ if (snippets.TryGetValue(identifier, out values)) {
+
+ XmlWriter writer = node.InsertAfter();
+ writer.WriteStartElement("snippets");
+ writer.WriteAttributeString("reference", reference);
+
+ foreach (StoredSnippet value in values) {
+ writer.WriteStartElement("snippet");
+ writer.WriteAttributeString("language", value.Language);
+
+ if (colorization.ContainsKey(value.Language)) {
+ WriteColorizedSnippet(ColorizeSnippet(value.Text, colorization[value.Language]), writer);
+
+ } else {
+ writer.WriteString(value.Text);
+ //writer.WriteString(System.Web.HttpUtility.HtmlDecode(value.Text));
+ }
+ writer.WriteEndElement();
+ }
+
+ writer.WriteEndElement();
+ writer.Close();
+
+ } else {
+ WriteMessage(MessageLevel.Warn, String.Format("No snippet with identifier '{0}' was found.", identifier));
+ }
+ } else {
+ // multiple snippets referenced
+
+ // create structure that maps language -> snippets
+ Dictionary<string,List<StoredSnippet>> map = new Dictionary<string,List<StoredSnippet>>();
+ foreach (SnippetIdentifier identifier in identifiers) {
+ List<StoredSnippet> values;
+ if (snippets.TryGetValue(identifier, out values)) {
+ foreach (StoredSnippet value in values) {
+ List<StoredSnippet> pieces;
+ if (!map.TryGetValue(value.Language, out pieces)) {
+ pieces = new List<StoredSnippet>();
+ map.Add(value.Language, pieces);
+ }
+ pieces.Add(value);
+ }
+ }
+ }
+
+ XmlWriter writer = node.InsertAfter();
+ writer.WriteStartElement("snippets");
+ writer.WriteAttributeString("reference", reference);
+
+ foreach (KeyValuePair<string,List<StoredSnippet>> entry in map) {
+ writer.WriteStartElement("snippet");
+ writer.WriteAttributeString("language", entry.Key);
+
+ List<StoredSnippet> values = entry.Value;
+ for (int i=0; i<values.Count; i++) {
+ if (i>0) writer.WriteString("\n...\n\n\n");
+ writer.WriteString(values[i].Text);
+ // writer.WriteString(System.Web.HttpUtility.HtmlDecode(values[i].Text));
+ }
+
+ writer.WriteEndElement();
+ }
+
+ writer.WriteEndElement();
+ writer.Close();
+
+ }
+
+ } else {
+ WriteMessage(MessageLevel.Warn, String.Format("The code reference '{0}' is not well-formed", reference));
+ }
+
+ node.DeleteSelf();
+
+ }
+ }
+
+ private XPathExpression selector;
+
+ private XmlNamespaceManager context = new CustomContext();
+
+ private static Regex validSnippetReference = new Regex(@"^[^#\a\b\f\n\r\t\v]+#(\w+,)*\w+$", RegexOptions.Compiled);
+
+ // colorization logic
+
+ private Dictionary<string,List<ColorizationRule>> colorization = new Dictionary<string,List<ColorizationRule>>();
+
+ private static ICollection<Region> ColorizeSnippet (string text, List<ColorizationRule> rules) {
+
+ // Console.WriteLine("colorizing: '{0}'", text);
+
+ // create a linked list consiting entirely of one uncolored region
+ LinkedList<Region> regions = new LinkedList<Region>();
+ regions.AddFirst( new Region(text) );
+
+ // loop over colorization rules
+ foreach (ColorizationRule rule in rules) {
+
+ // loop over regions
+
+ LinkedListNode<Region> node = regions.First;
+ while(node != null) {
+
+ // only try to colorize uncolored regions
+ if (node.Value.ClassName != null) {
+ node = node.Next;
+ continue;
+ }
+
+ // find matches in the region
+ string regionText = node.Value.Text;
+ Capture[] matches = rule.Apply(regionText);
+
+ // if no matches were found, continue to the next region
+ if (matches.Length == 0) {
+ node = node.Next;
+ continue;
+ }
+
+ // we found matches; break the region into colored and uncolered subregions
+
+ // index is where we are looking from; index-1 is the end of the last match
+ int index = 0;
+
+ LinkedListNode<Region> referenceNode = node;
+
+ foreach (Capture match in matches) {
+
+ // create a leading uncolored region
+ if (match.Index > index) {
+ //Console.WriteLine("uncolored: {0} '{1}' -> {2} '{3}'", index, regionText[index], match.Index - 1, regionText[match.Index - 1]);
+ Region uncoloredRegion = new Region(regionText.Substring(index, match.Index-index));
+ referenceNode = regions.AddAfter(referenceNode, uncoloredRegion);
+ }
+
+ // create a colored region
+ // Console.WriteLine("name = {0}", rule.ClassName);
+ //Console.WriteLine("colored: {0} '{1}' -> {2} '{3}'", match.Index, regionText[match.Index], match.Index + match.Length - 1, regionText[match.Index + match.Length - 1]);
+ Region coloredRegion = new Region(rule.ClassName, regionText.Substring(match.Index, match.Length));
+ referenceNode = regions.AddAfter(referenceNode, coloredRegion);
+
+ index = match.Index + match.Length;
+
+ }
+
+ // create a trailing uncolored region
+ if (index < regionText.Length) {
+ Region uncoloredRegion = new Region(regionText.Substring(index));
+ referenceNode = regions.AddAfter(referenceNode, uncoloredRegion);
+ }
+
+ // remove the original node
+ regions.Remove(node);
+
+ node = referenceNode.Next;
+ }
+
+
+ }
+ return(regions);
+
+ }
+
+ private static void WriteColorizedSnippet (ICollection<Region> regions, XmlWriter writer) {
+ foreach (Region region in regions) {
+ // Console.WriteLine("writing {0}", region.ClassName);
+ if (region.ClassName == null) {
+ writer.WriteString(region.Text);
+ } else {
+ writer.WriteStartElement("span");
+ writer.WriteAttributeString("class", region.ClassName);
+ writer.WriteString(region.Text);
+ writer.WriteEndElement();
+ }
+ }
+ }
+
+ private static string StripLeadingSpaces (string text) {
+
+ if (text == null) throw new ArgumentNullException("text");
+
+ // Console.WriteLine("BEFORE:");
+ // Console.WriteLine(text);
+
+ // split the text into lines
+ string[] lines = text.Split('\n');
+
+ // no need to do this if there is only one line
+ if (lines.Length == 1) return(lines[0]);
+
+ // figure out how many leading spaces to delete
+ int spaces = Int32.MaxValue;
+ for (int i=0; i<lines.Length; i++) {
+
+ string line = lines[i];
+
+ // skip empty lines
+ if (line.Length == 0) continue;
+
+ // determine the number of leading spaces
+ int index = 0;
+ while (index < line.Length) {
+ if (line[index] != ' ') break;
+ index++;
+ }
+
+ if (index == line.Length) {
+ // lines that are all spaces should just be treated as empty
+ lines[i] = String.Empty;
+ } else {
+ // otherwise, keep track of the minimum number of leading spaces
+ if (index < spaces) spaces = index;
+ }
+
+ }
+
+ // Console.WriteLine("SPACES = {0}", spaces);
+
+ // re-form the string with leading spaces deleted
+ StringBuilder result = new StringBuilder();
+ foreach (string line in lines) {
+ if (line.Length == 0) {
+ result.AppendLine();
+ } else {
+ result.AppendLine(line.Substring(spaces));
+ }
+ }
+ // Console.WriteLine("AFTER:");
+ // Console.WriteLine(result.ToString());
+ return(result.ToString());
+
+ }
+
+ }
+
+ internal struct SnippetIdentifier {
+
+ public SnippetIdentifier (string exampleId, string snippetId) {
+ this.exampleId = exampleId.ToLower();
+ this.snippetId = snippetId.ToLower();
+ }
+
+ public SnippetIdentifier (string identifier) {
+ int index = identifier.LastIndexOf('#');
+ exampleId = identifier.Substring(0,index).ToLower();
+ snippetId = identifier.Substring(index+1).ToLower();
+ }
+
+ private string exampleId;
+
+ private string snippetId;
+
+ public string Example {
+ get {
+ return(exampleId);
+ }
+ }
+
+ public string Snippet {
+ get {
+ return(snippetId);
+ }
+ }
+
+ public override string ToString() {
+ return(String.Format("{0}#{1}", exampleId, snippetId));
+ }
+
+ public static SnippetIdentifier[] ParseReference (string reference) {
+
+ int index = reference.IndexOf('#');
+ if (index < 0) return(new SnippetIdentifier[0]);
+
+ string example = reference.Substring(0,index);
+ string[] snippets = reference.Substring(index+1).Split(',');
+
+ SnippetIdentifier[] identifiers = new SnippetIdentifier[snippets.Length];
+ for (int i=0; i<snippets.Length; i++) {
+ identifiers[i] = new SnippetIdentifier(example, snippets[i]);
+ }
+ return(identifiers);
+
+ }
+
+ }
+
+ internal class StoredSnippet {
+
+ public StoredSnippet (string text, string language) {
+ this.text = text;
+ this.language = language;
+ }
+
+ private string text;
+
+ private Region[] regions;
+
+ private string language;
+
+ public string Text {
+ get {
+ return(text);
+ }
+ }
+
+ public string Language {
+ get {
+ return(language);
+ }
+ }
+
+ public Region[] Regions {
+ get {
+ return(regions);
+ }
+ }
+
+ }
+
+ internal class ColorizationRule {
+
+ public ColorizationRule (string pattern, string className) : this(pattern, null, className) {}
+
+ public ColorizationRule (string pattern, string region, string className) {
+ this.pattern = new Regex(pattern, RegexOptions.Compiled|RegexOptions.Multiline);
+ this.region = region;
+ this.className = className;
+ }
+
+ private Regex pattern;
+
+ private string region;
+
+ private string className;
+
+ public string ClassName {
+ get {
+ return(className);
+ }
+ }
+
+ public Capture[] Apply (string text) {
+
+ MatchCollection matches = pattern.Matches(text);
+ Capture[] captures = new Capture[matches.Count];
+
+ if (region == null) {
+ matches.CopyTo(captures, 0);
+ return(captures);
+ } else {
+ for (int i=0; i<captures.Length; i++) {
+ captures[i] = matches[i].Groups[region];
+ }
+ return(captures);
+ }
+
+ }
+
+ }
+
+ internal struct Region {
+
+ public Region (string text) : this(null, text) {}
+
+ public Region (string className, string text) {
+ this.className = className;
+ this.text = text;
+ }
+
+ private string className;
+
+ private string text;
+
+
+ public string ClassName {
+ get {
+ return(className);
+ }
+ }
+
+ public string Text {
+ get {
+ return(text);
+ }
+ }
+
+ }
+
+}