/// <summary> /// Renders the code /// </summary> /// <remarks>You must specify a colorizing engine setting the <see cref="SyntaxEngine" /> property /// before calling this method.</remarks> /// <param name="writer">HTML writer</param> protected override void Render(HtmlTextWriter writer) { if (!String.IsNullOrEmpty(text)) { if (colorizer == null) { writer.Write("<strong>Colorizer not set!</strong><br/>"); writer.Write(text); } else { writer.Write(colorizer.ProcessAndHighlightText(text)); #if DEBUG && BENCHMARK System.Diagnostics.Debug.WriteLine("Performance: " + colorizer.BenchmarkSec + " s, " + colorizer.BenchmarkSecPerChar + " s/char, " + colorizer.BenchmarkAvgSec + " s"); #endif } } }
/// <summary> /// This is called to load an additional content file, resolve links /// to namespace content and copy it to the output folder. /// </summary> /// <param name="sourceFile">The source filename to copy</param> /// <param name="destFile">The destination filename</param> /// <param name="entry">The entry being resolved.</param> private void ResolveLinksAndCopy(string sourceFile, string destFile, TocEntry entry) { Encoding enc = Encoding.Default; string content, script, syntaxFile; int pos; this.ReportProgress("{0} -> {1}", sourceFile, destFile); // When reading the file, use the default encoding but detect the // encoding if byte order marks are present. content = BuildProcess.ReadWithEncoding(sourceFile, ref enc); // Use a regular expression to find and replace all <see> // tags with link to the help file content. if (entry.HasLinks) { content = reResolveLinks.Replace(content, linkMatchEval); } // Expand <code> tags if necessary if (entry.HasCodeBlocks) { content = reCodeBlock.Replace(content, codeBlockMatchEval); } // Colorize <pre> tags if necessary if (entry.NeedsColorizing || entry.HasCodeBlocks) { // Initialize code colorizer on first use if (codeColorizer == null) { codeColorizer = new CodeColorizer( shfbFolder + @"Colorizer\highlight.xml", shfbFolder + @"Colorizer\highlight.xsl"); } // Set the path the "Copy" image codeColorizer.CopyImageUrl = pathToRoot + "icons/CopyCode.gif"; // Colorize it and replace the "Copy" literal text with the // shared content include item so that it gets localized. content = codeColorizer.ProcessAndHighlightText(content); content = content.Replace(codeColorizer.CopyText + "</span", "<include item=\"copyCode\"/></span"); entry.HasProjectTags = true; // Add the links to the colorizer stylesheet and script files script = String.Format(CultureInfo.InvariantCulture, "<link type='text/css' rel='stylesheet' " + "href='{0}html/highlight.css' />" + "<script type='text/javascript' " + "src='{0}html/highlight.js'></script>", pathToRoot); pos = content.IndexOf("</head>"); // Create a <head> section if one doesn't exist if (pos == -1) { script = "<head>" + script + "</head>"; pos = content.IndexOf("<html>"); if (pos != -1) { pos += 6; } else { pos = 0; } } content = content.Insert(pos, script); // Copy the colorizer files if not already there if (!File.Exists(workingFolder + @"Output\html\highlight.css")) { syntaxFile = shfbFolder + @"Colorizer\highlight.css"; File.Copy(syntaxFile, workingFolder + @"Output\html\highlight.css"); File.SetAttributes(syntaxFile, FileAttributes.Normal); syntaxFile = shfbFolder + @"Colorizer\highlight.js"; File.Copy(syntaxFile, workingFolder + @"Output\html\highlight.js"); File.SetAttributes(syntaxFile, FileAttributes.Normal); // This one may exist as the default presentation styles // contain an image by this name. syntaxFile = shfbFolder + @"Colorizer\CopyCode.gif"; File.Copy(syntaxFile, workingFolder + @"Output\icons\CopyCode.gif", true); File.SetAttributes(syntaxFile, FileAttributes.Normal); } } // Replace project option tags with project option values if (entry.HasProjectTags) { content = reProjectTags.Replace(content, fieldMatchEval); // Shared content items can be nested while (reSharedContent.IsMatch(content)) { content = reSharedContent.Replace(content, contentMatchEval); } } // Write the file back out with the appropriate encoding using (StreamWriter sw = new StreamWriter(destFile, false, enc)) { sw.Write(content); } }
/// <summary> /// This is implemented to perform the code colorization. /// </summary> /// <param name="document">The XML document with which to work.</param> /// <param name="key">The key (member name) of the item being /// documented.</param> public override void Apply(XmlDocument document, string key) { XPathNavigator root, navDoc = document.CreateNavigator(); XPathNavigator[] codeList; XPathExpression nestedCode; XmlAttribute attr; XmlNode code, titleDiv, preNode, container, refLink; string language, title, codeBlock; bool nbrLines, outline, seeTags, filter; int tabSize, start, end, blockId = 1, id = 1; // Clear the dictionary colorizedCodeBlocks.Clear(); // Select all code nodes. The location depends on the build type. root = navDoc.SelectSingleNode(referenceRoot); // If not null, it's a reference (API) build. If null, it's // a conceptual build. if (root != null) { codeList = BuildComponentUtilities.ConvertNodeIteratorToArray( root.Select(referenceCode)); nestedCode = nestedRefCode; } else { root = navDoc.SelectSingleNode(conceptualRoot); nestedCode = nestedConceptCode; if (root == null) { base.WriteMessage(MessageLevel.Warn, "Root content node not found. Cannot colorize code."); return; } codeList = BuildComponentUtilities.ConvertNodeIteratorToArray( root.Select(conceptualCode)); } foreach (XPathNavigator navCode in codeList) { code = ((IHasXmlNode)navCode).GetNode(); // If the parent is null, it was a nested node and has already // been handled. if (code.ParentNode == null) { continue; } // Set the defaults language = defaultLanguage; nbrLines = numberLines; outline = outliningEnabled; seeTags = keepSeeTags; filter = languageFilter; tabSize = 0; title = String.Empty; // If there are nested code blocks, load them. Source and // region attributes will be ignored on the parent. All // other attributes will be applied to the combined block of // code. If there are no nested blocks, source and region // will be used to load the code if found. Otherwise, the // existing inner XML is used for the code. if (navCode.SelectSingleNode(nestedCode) != null) { codeBlock = this.LoadNestedCodeBlocks(navCode, nestedCode); } else if (code.Attributes["source"] != null) { codeBlock = this.LoadCodeBlock(code); } else { codeBlock = code.InnerXml; } // Check for option overrides if (code.Attributes["numberLines"] != null) { nbrLines = Convert.ToBoolean(code.Attributes[ "numberLines"].Value, CultureInfo.InvariantCulture); } if (code.Attributes["outlining"] != null) { outline = Convert.ToBoolean(code.Attributes[ "outlining"].Value, CultureInfo.InvariantCulture); } if (code.Attributes["keepSeeTags"] != null) { seeTags = Convert.ToBoolean(code.Attributes[ "keepSeeTags"].Value, CultureInfo.InvariantCulture); } if (code.Attributes["filter"] != null) { filter = Convert.ToBoolean(code.Attributes[ "filter"].Value, CultureInfo.InvariantCulture); } if (code.Attributes["tabSize"] != null) { tabSize = Convert.ToInt32(code.Attributes["tabSize"].Value, CultureInfo.InvariantCulture); } // If either language option is set to "none" or an unknown // language, it just strips excess leading whitespace and // optionally numbers the lines and adds outlining based on // the other settings. if (code.Attributes["lang"] != null) { language = code.Attributes["lang"].Value; } else if (code.Attributes["language"] != null) { language = code.Attributes["language"].Value; } // Use the title if one is supplied. if (code.Attributes["title"] != null) { title = HttpUtility.HtmlEncode(code.Attributes["title"].Value); } // Process the code. The colorizer is built to highlight <pre> tags in an HTML file so we'll // wrap the code in a <pre> tag with the settings. codeBlock = colorizer.ProcessAndHighlightText(String.Format(CultureInfo.InvariantCulture, "<pre lang=\"{0}\" numberLines=\"{1}\" outlining=\"{2}\" " + "keepSeeTags=\"{3}\" tabSize=\"{4}\" {5}>{6}</pre>", language, nbrLines, outline, seeTags, tabSize, (title.Length != 0) ? "title=\"" + title + "\"" : String.Empty, codeBlock)); // Non-breaking spaces are replaced with a space entity. If not, they disappear in the rendered // HTML. Seems to be an XML or XSLT thing. codeBlock = codeBlock.Replace(" ", " "); // Move the title above the code block so that it doesn't interfere with the stuff generated // by the transformation component. The post-transform component will perform some clean-up // to get rid of similar stuff added by it. title = codeBlock.Substring(0, codeBlock.IndexOf("</div>", StringComparison.Ordinal) + 6); codeBlock = codeBlock.Substring(title.Length); titleDiv = document.CreateDocumentFragment(); titleDiv.InnerXml = title; titleDiv = titleDiv.ChildNodes[0]; // Remove the colorizer's <pre> tag. We'll add our own below. start = codeBlock.IndexOf('>') + 1; end = codeBlock.LastIndexOf('<'); // We need to add the xml:space="preserve" attribute on the <pre> element so that MS Help Viewer // doesn't remove significant whitespace between colorized elements. preNode = document.CreateNode(XmlNodeType.Element, "pre", null); attr = document.CreateAttribute("xml:space"); attr.Value = "preserve"; preNode.Attributes.Append(attr); preNode.InnerXml = codeBlock.Substring(start, end - start); // Convert <see> tags to <referenceLink> or <a> tags. We need to do this so that the Resolve // Links component can do its job further down the line. The code blocks are not present when // the transformations run so that the colorized HTML doesn't get stripped out in conceptual // builds. This could be redone to use a <markup> element if the Sandcastle transformations // ever support it natively. foreach (XmlNode seeTag in preNode.SelectNodes("//see")) { if (seeTag.Attributes["cref"] != null) { refLink = document.CreateElement("referenceLink"); } else { refLink = document.CreateElement("a"); } foreach (XmlAttribute seeAttr in seeTag.Attributes) { if (seeAttr.Name == "cref") { attr = document.CreateAttribute("target"); } else { attr = (XmlAttribute)seeAttr.Clone(); } attr.Value = seeAttr.Value; refLink.Attributes.Append(attr); } if (seeTag.HasChildNodes) { refLink.InnerXml = seeTag.InnerXml; } seeTag.ParentNode.ReplaceChild(refLink, seeTag); } // The <span> tags cannot be self-closing if empty. The colorizer renders them correctly but // when written out as XML, they get converted to self-closing tags which breaks them. To fix // them, store an empty string in each empty span so that it renders as an opening and closing // tag. Note that if null, InnerText returns an empty string by default. As such, this looks // redundant but it really isn't (see note above). foreach (XmlNode span in preNode.SelectNodes("//span")) { if (span.InnerText.Length == 0) { span.InnerText = String.Empty; } } // Add language filter stuff if needed or just the title if there is one and we aren't // using the language filter. if (filter) { // If necessary, map the language ID to one we will recognize language = language.ToLower(CultureInfo.InvariantCulture); if (colorizer.AlternateIds.ContainsKey(language)) { language = colorizer.AlternateIds[language]; } container = this.AddLanguageFilter(titleDiv, preNode, language, blockId++); } else { container = document.CreateNode(XmlNodeType.Element, "span", null); container.AppendChild(titleDiv); container.AppendChild(preNode); } // Replace the code with a placeholder ID. The post-transform // component will relace it with the code from the container. code.InnerXml = "@@_SHFB_" + id.ToString(CultureInfo.InvariantCulture); // Add the container to the code block dictionary colorizedCodeBlocks.Add(code.InnerXml, container); id++; } }
/// <summary> /// This is called to load an additional content file, resolve links to namespace content and copy it to /// the output folder. /// </summary> /// <param name="sourceFile">The source filename to copy</param> /// <param name="destFile">The destination filename</param> /// <param name="entry">The entry being resolved.</param> internal void ResolveLinksAndCopy(string sourceFile, string destFile, TocEntry entry) { Encoding enc = Encoding.Default; string content, script, syntaxFile; int pos; // For topics, change the extension back to ".topic". It's ".html" in the TOC as that's what it ends // up as after transformation. if (sourceFile.EndsWith(".topic", StringComparison.OrdinalIgnoreCase)) { destFile = Path.ChangeExtension(destFile, ".topic"); } this.ReportProgress("{0} -> {1}", sourceFile, destFile); // When reading the file, use the default encoding but detect the encoding if byte order marks are // present. content = BuildProcess.ReadWithEncoding(sourceFile, ref enc); // Expand <code> tags if necessary if (entry.HasCodeBlocks) { content = reCodeBlock.Replace(content, codeBlockMatchEval); } // Colorize <pre> tags if necessary if (entry.NeedsColorizing || entry.HasCodeBlocks) { // Initialize code colorizer on first use if (codeColorizer == null) { codeColorizer = new CodeColorizer(ComponentUtilities.ToolsFolder + @"PresentationStyles\Colorizer\highlight.xml", ComponentUtilities.ToolsFolder + @"PresentationStyles\Colorizer\highlight.xsl"); } // Set the path the "Copy" image codeColorizer.CopyImageUrl = pathToRoot + "icons/CopyCode.gif"; // Colorize it and replace the "Copy" literal text with the shared content include item so that // it gets localized. content = codeColorizer.ProcessAndHighlightText(content); content = content.Replace(codeColorizer.CopyText + "</span", "<include item=\"copyCode\"/></span"); entry.HasProjectTags = true; // Add the links to the colorizer style sheet and script files unless it's going to be // transformed. In which case, the links should be in the XSL style sheet. if (!sourceFile.EndsWith(".topic", StringComparison.OrdinalIgnoreCase) && !sourceFile.EndsWith(".xsl", StringComparison.OrdinalIgnoreCase)) { script = String.Format(CultureInfo.InvariantCulture, "<link type='text/css' rel='stylesheet' href='{0}styles/highlight.css' />" + "<script type='text/javascript' src='{0}scripts/highlight_ac.js'></script>", pathToRoot); pos = content.IndexOf("</head>", StringComparison.Ordinal); // Create a <head> section if one doesn't exist if (pos == -1) { script = "<head>" + script + "</head>"; pos = content.IndexOf("<html>", StringComparison.Ordinal); if (pos != -1) { pos += 6; } else { pos = 0; } } content = content.Insert(pos, script); } // Copy the colorizer files if not already there this.EnsureOutputFoldersExist("icons"); this.EnsureOutputFoldersExist("styles"); this.EnsureOutputFoldersExist("scripts"); foreach (string baseFolder in this.HelpFormatOutputFolders) { if (!File.Exists(baseFolder + @"styles\highlight.css")) { syntaxFile = baseFolder + @"styles\highlight.css"; File.Copy(ComponentUtilities.ToolsFolder + @"PresentationStyles\Colorizer\highlight.css", syntaxFile); File.SetAttributes(syntaxFile, FileAttributes.Normal); syntaxFile = baseFolder + @"scripts\highlight_ac.js"; File.Copy(ComponentUtilities.ToolsFolder + @"PresentationStyles\Colorizer\highlight_ac.js", syntaxFile); File.SetAttributes(syntaxFile, FileAttributes.Normal); // Always copy the image files, they may be different. Also, delete the destination file // first if it exists as the filename casing may be different. syntaxFile = baseFolder + @"icons\CopyCode.gif"; if (File.Exists(syntaxFile)) { File.SetAttributes(syntaxFile, FileAttributes.Normal); File.Delete(syntaxFile); } File.Copy(ComponentUtilities.ToolsFolder + @"PresentationStyles\Colorizer\CopyCode.gif", syntaxFile); File.SetAttributes(syntaxFile, FileAttributes.Normal); syntaxFile = baseFolder + @"icons\CopyCode_h.gif"; if (File.Exists(syntaxFile)) { File.SetAttributes(syntaxFile, FileAttributes.Normal); File.Delete(syntaxFile); } File.Copy(ComponentUtilities.ToolsFolder + @"PresentationStyles\Colorizer\CopyCode_h.gif", syntaxFile); File.SetAttributes(syntaxFile, FileAttributes.Normal); } } } // Use a regular expression to find and replace all tags with cref attributes with a link to the help // file content. This needs to happen after the code block processing as they may contain <see> tags // that need to be resolved. if (entry.HasLinks || entry.HasCodeBlocks) { content = reResolveLinks.Replace(content, linkMatchEval); } // Replace project option tags with project option values if (entry.HasProjectTags) { // Project tags can be nested while (reProjectTags.IsMatch(content)) { content = reProjectTags.Replace(content, fieldMatchEval); } // Shared content items can be nested while (reSharedContent.IsMatch(content)) { content = reSharedContent.Replace(content, contentMatchEval); } } // Write the file back out with the appropriate encoding using (StreamWriter sw = new StreamWriter(destFile, false, enc)) { sw.Write(content); } // Transform .topic files into .html files if (sourceFile.EndsWith(".topic", StringComparison.OrdinalIgnoreCase)) { this.XslTransform(destFile); } }
/// <summary> /// This is implemented to perform the code colorization. /// </summary> /// <param name="document">The XML document with which to work.</param> /// <param name="key">The key (member name) of the item being /// documented.</param> public override void Apply(XmlDocument document, string key) { XmlNodeList codeList; XmlNode titleDiv; string language, title, codeBlock; bool nbrLines, outline; int tabSize, start, end; // Reset the flag needsHighlightFiles = false; // Select all code nodes codeList = document.SelectNodes("//code"); foreach (XmlNode code in codeList) { // Set the defaults language = defaultLanguage; nbrLines = numberLines; outline = outliningEnabled; tabSize = defaultTabSize; title = String.Empty; // If there's a source attribute, load the code from the file if (code.Attributes["source"] != null) { codeBlock = this.LoadCodeBlock(code); } else { codeBlock = code.InnerXml; } // Check for option overrides if (code.Attributes["numberLines"] != null) { nbrLines = Convert.ToBoolean( code.Attributes["numberLines"].Value, CultureInfo.InvariantCulture); } if (code.Attributes["outlining"] != null) { outline = Convert.ToBoolean( code.Attributes["outlining"].Value, CultureInfo.InvariantCulture); } if (code.Attributes["tabSize"] != null) { tabSize = Convert.ToInt32( code.Attributes["numberLines"].Value, CultureInfo.InvariantCulture); } // If either language option is set to "none" or an unknown // language, it just strips excess leading whitespace and // optionally numbers the lines and adds outlining based on // the other settings. if (code.Attributes["lang"] != null) { language = code.Attributes["lang"].Value; } // Use the title if one is supplied. if (code.Attributes["title"] != null) { title = HttpUtility.HtmlEncode( code.Attributes["title"].Value); } // Process the code. The colorizer is built to highlight // <pre> tags in an HTML file so we'll wrap the code in a // <pre> tag with the settings. codeBlock = colorizer.ProcessAndHighlightText( String.Format(CultureInfo.InvariantCulture, "<pre lang=\"{0}\" numberLines=\"{1}\" " + "outlining=\"{2}\" tabSize=\"{3}\" {4}>{5}</pre>", language, nbrLines, outline, tabSize, (title.Length != 0) ? "title=\"" + title + "\"" : String.Empty, codeBlock)); // Non-breaking spaces are replaced with a space entity. If // not, they disappear in the rendered HTML. Seems to be an // XML or XSLT thing. codeBlock = codeBlock.Replace(" ", " "); // Move the title above the code block so that it doesn't // interfere with the stuff generated by the transformation // component. The post-transform component will perform some // clean-up to get rid of similar stuff added by it. if (codeBlock.StartsWith("<div")) { title = codeBlock.Substring(0, codeBlock.IndexOf( "</div>") + 6); codeBlock = codeBlock.Substring(title.Length); titleDiv = document.CreateDocumentFragment(); titleDiv.InnerXml = title; titleDiv = titleDiv.ChildNodes[0]; } else { titleDiv = null; } // Remove the colorizer's <pre> tag and put it back into // the document. The Sandcastle transform puts the code in // <div> and <pre> tags itself. start = codeBlock.IndexOf(">") + 1; end = codeBlock.LastIndexOf("<"); code.InnerXml = codeBlock.Substring(start, end - start); // The <span> tags cannot be self-closing if empty. The // colorizer renders them correctly but when written out as // XML, they get converted to self-closing tags which breaks // them. To fix them, store an empty string in each empty // span so that it renders as an opening and closing tag. XmlNodeList spans = code.SelectNodes("//span"); // Note that if null, InnerText returns an empty string // by default. As such, this looks redundant but it // really isn't (see note above). foreach (XmlNode span in spans) { if (span.InnerText.Length == 0) { span.InnerText = String.Empty; } } // Add language filter stuff if needed or just the title if // there is one and we aren't using the language filter. if (languageFilter) { CodeBlockComponent.AddLanguageFilter(titleDiv, code, language); } else if (titleDiv != null) { code.ParentNode.InsertBefore(titleDiv, code); } // Set the flag if needed to have the PostTransformComponent // include the stylesheet and script files. if (!needsHighlightFiles && (outline || language != "none" || titleDiv != null)) { needsHighlightFiles = true; } } }