#region License Information
// Copyright (c) 2014 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
/* HeuristicLab
* Copyright (C) 2002-2018 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
*
* This file is part of HeuristicLab.
*
* HeuristicLab is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* HeuristicLab is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with HeuristicLab. If not, see .
*/
#endregion
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Utils;
using ICSharpCode.NRefactory.Editor;
using ICSharpCode.NRefactory.TypeSystem;
using ICSharpCode.NRefactory.Xml;
namespace HeuristicLab.CodeEditor {
///
/// Builds a FlowDocument for XML documentation.
///
public class CSharpDocumentationBuilder {
const string SyntaxHighlighting = "C#";
const string FontFamily = "Consolas";
const double FontSize = 13.0;
const string MsdnLibraryUrl = "http://msdn.microsoft.com/library/";
readonly FlowDocument flowDocument;
readonly IAmbience ambience;
BlockCollection blockCollection;
InlineCollection inlineCollection;
public CSharpDocumentationBuilder(IAmbience ambience) {
this.ambience = ambience;
this.flowDocument = new FlowDocument();
this.blockCollection = flowDocument.Blocks;
this.ShowSummary = true;
this.ShowAllParameters = true;
this.ShowReturns = true;
this.ShowThreadSafety = true;
this.ShowExceptions = true;
this.ShowTypeParameters = true;
this.ShowExample = true;
this.ShowPreliminary = true;
this.ShowSeeAlso = true;
this.ShowValue = true;
this.ShowPermissions = true;
this.ShowRemarks = true;
}
public FlowDocument CreateFlowDocument() {
FlushAddedText(true);
flowDocument.FontSize = FontSize;
return flowDocument;
}
public bool ShowExceptions { get; set; }
public bool ShowPermissions { get; set; }
public bool ShowExample { get; set; }
public bool ShowPreliminary { get; set; }
public bool ShowRemarks { get; set; }
public bool ShowSummary { get; set; }
public bool ShowReturns { get; set; }
public bool ShowSeeAlso { get; set; }
public bool ShowThreadSafety { get; set; }
public bool ShowTypeParameters { get; set; }
public bool ShowValue { get; set; }
public bool ShowAllParameters { get; set; }
///
/// Gets/Sets the name of the parameter that should be shown.
///
public string ParameterName { get; set; }
public void AddDocumentationElement(XmlDocumentationElement element) {
if (element == null)
throw new ArgumentNullException("element");
if (element.IsTextNode) {
AddText(element.TextContent);
return;
}
switch (element.Name) {
case "b":
AddSpan(new Bold(), element.Children);
break;
case "i":
AddSpan(new Italic(), element.Children);
break;
case "c":
AddSpan(new Span { FontFamily = GetCodeFont() }, element.Children);
break;
case "code":
AddCodeBlock(element.TextContent);
break;
case "example":
if (ShowExample)
AddSection("Example: ", element.Children);
break;
case "exception":
if (ShowExceptions)
AddException(element.ReferencedEntity, element.Children);
break;
case "list":
AddList(element.GetAttribute("type"), element.Children);
break;
//case "note":
// throw new NotImplementedException();
case "para":
AddParagraph(new Paragraph { Margin = new Thickness(0, 5, 0, 5) }, element.Children);
break;
case "param":
if (ShowAllParameters || (ParameterName != null && ParameterName == element.GetAttribute("name")))
AddParam(element.GetAttribute("name"), element.Children);
break;
case "paramref":
AddParamRef(element.GetAttribute("name"));
break;
case "permission":
if (ShowPermissions)
AddPermission(element.ReferencedEntity, element.Children);
break;
case "preliminary":
if (ShowPreliminary)
AddPreliminary(element.Children);
break;
case "remarks":
if (ShowRemarks)
AddSection("Remarks: ", element.Children);
break;
case "returns":
if (ShowReturns)
AddSection("Returns: ", element.Children);
break;
case "see":
AddSee(element);
break;
case "seealso":
if (inlineCollection != null)
AddSee(element);
else if (ShowSeeAlso)
AddSection(new Run("See also: "), () => AddSee(element));
break;
case "summary":
if (ShowSummary)
AddSection("Summary: ", element.Children);
break;
case "threadsafety":
if (ShowThreadSafety)
AddThreadSafety(ParseBool(element.GetAttribute("static")), ParseBool(element.GetAttribute("instance")), element.Children);
break;
case "typeparam":
if (ShowTypeParameters)
AddSection("Type parameter " + element.GetAttribute("name") + ": ", element.Children);
break;
case "typeparamref":
AddText(element.GetAttribute("name"));
break;
case "value":
if (ShowValue)
AddSection("Value: ", element.Children);
break;
case "exclude":
case "filterpriority":
case "overloads":
// ignore children
break;
case "br":
AddLineBreak();
break;
default:
foreach (var child in element.Children)
AddDocumentationElement(child);
break;
}
}
void AddList(string type, IEnumerable items) {
List list = new List();
AddBlock(list);
list.Margin = new Thickness(0, 5, 0, 5);
if (type == "number")
list.MarkerStyle = TextMarkerStyle.Decimal;
else if (type == "bullet")
list.MarkerStyle = TextMarkerStyle.Disc;
var oldBlockCollection = blockCollection;
try {
foreach (var itemElement in items) {
if (itemElement.Name == "listheader" || itemElement.Name == "item") {
ListItem item = new ListItem();
blockCollection = item.Blocks;
inlineCollection = null;
foreach (var prop in itemElement.Children) {
AddDocumentationElement(prop);
}
FlushAddedText(false);
list.ListItems.Add(item);
}
}
} finally {
blockCollection = oldBlockCollection;
}
}
public void AddCodeBlock(string textContent, bool keepLargeMargin = false) {
var document = new ReadOnlyDocument(textContent);
var highlightingDefinition = HighlightingManager.Instance.GetDefinition(SyntaxHighlighting);
var block = DocumentPrinter.ConvertTextDocumentToBlock(document, highlightingDefinition);
block.FontFamily = GetCodeFont();
if (!keepLargeMargin)
block.Margin = new Thickness(0, 6, 0, 6);
AddBlock(block);
}
public void AddSignatureBlock(string signature, int currentParameterOffset, int currentParameterLength, string currentParameterName) {
ParameterName = currentParameterName;
var document = new ReadOnlyDocument(signature);
var highlightingDefinition = HighlightingManager.Instance.GetDefinition(SyntaxHighlighting);
var richText = DocumentPrinter.ConvertTextDocumentToRichText(document, highlightingDefinition).ToRichTextModel();
richText.SetFontWeight(currentParameterOffset, currentParameterLength, FontWeights.Bold);
var block = new Paragraph();
block.Inlines.AddRange(richText.CreateRuns(document));
block.FontFamily = GetCodeFont();
block.TextAlignment = TextAlignment.Left;
AddBlock(block);
}
bool? ParseBool(string input) {
bool result;
if (bool.TryParse(input, out result))
return result;
else
return null;
}
void AddThreadSafety(bool? staticThreadSafe, bool? instanceThreadSafe, IEnumerable children) {
AddSection(
new Run("Thread-safety: "),
delegate {
if (staticThreadSafe == true)
AddText("Any public static members of this type are thread safe. ");
else if (staticThreadSafe == false)
AddText("The static members of this type are not thread safe. ");
if (instanceThreadSafe == true)
AddText("Any public instance members of this type are thread safe. ");
else if (instanceThreadSafe == false)
AddText("Any instance members are not guaranteed to be thread safe. ");
foreach (var child in children)
AddDocumentationElement(child);
});
}
FontFamily GetCodeFont() {
return new FontFamily(FontFamily);
}
void AddException(IEntity referencedEntity, IList children) {
Span span = new Span();
if (referencedEntity != null)
span.Inlines.Add(ConvertReference(referencedEntity));
else
span.Inlines.Add("Exception");
span.Inlines.Add(": ");
AddSection(span, children);
}
void AddPermission(IEntity referencedEntity, IList children) {
Span span = new Span();
span.Inlines.Add("Permission");
if (referencedEntity != null) {
span.Inlines.Add(" ");
span.Inlines.Add(ConvertReference(referencedEntity));
}
span.Inlines.Add(": ");
AddSection(span, children);
}
Inline ConvertReference(IEntity referencedEntity) {
var h = new Hyperlink(new Run(ambience.ConvertSymbol(referencedEntity)));
h.Click += CreateNavigateOnClickHandler(referencedEntity);
return h;
}
void AddParam(string name, IEnumerable children) {
Span span = new Span();
span.Inlines.Add(new Run(name ?? string.Empty) { FontStyle = FontStyles.Italic });
span.Inlines.Add(": ");
AddSection(span, children);
}
void AddParamRef(string name) {
if (name != null) {
AddInline(new Run(name) { FontStyle = FontStyles.Italic });
}
}
void AddPreliminary(IEnumerable children) {
if (children.Any()) {
foreach (var child in children)
AddDocumentationElement(child);
} else {
AddText("[This is preliminary documentation and subject to change.]");
}
}
void AddSee(XmlDocumentationElement element) {
IEntity referencedEntity = element.ReferencedEntity;
if (referencedEntity != null) {
if (element.Children.Any()) {
Hyperlink link = new Hyperlink();
link.Click += CreateNavigateOnClickHandler(referencedEntity);
AddSpan(link, element.Children);
} else {
AddInline(ConvertReference(referencedEntity));
}
} else if (element.GetAttribute("langword") != null) {
AddInline(new Run(element.GetAttribute("langword")) { FontFamily = GetCodeFont() });
} else if (element.GetAttribute("href") != null) {
Uri uri;
if (Uri.TryCreate(element.GetAttribute("href"), UriKind.Absolute, out uri)) {
if (element.Children.Any()) {
AddSpan(new Hyperlink { NavigateUri = uri }, element.Children);
} else {
AddInline(new Hyperlink(new Run(element.GetAttribute("href"))) { NavigateUri = uri });
}
}
} else {
// Invalid reference: print the cref value
AddText(element.GetAttribute("cref"));
}
}
RoutedEventHandler CreateNavigateOnClickHandler(IEntity referencedEntity) {
return delegate(object sender, RoutedEventArgs e) {
Process.Start(MsdnLibraryUrl + referencedEntity.FullName);
e.Handled = true;
};
}
void AddSection(string title, IEnumerable children) {
AddSection(new Run(title), children);
}
void AddSection(Inline title, IEnumerable children) {
AddSection(
title, delegate {
foreach (var child in children)
AddDocumentationElement(child);
});
}
void AddSection(Inline title, Action addChildren) {
var section = new Section();
AddBlock(section);
var oldBlockCollection = blockCollection;
try {
blockCollection = section.Blocks;
inlineCollection = null;
if (title != null)
AddInline(new Bold(title));
addChildren();
FlushAddedText(false);
} finally {
blockCollection = oldBlockCollection;
inlineCollection = null;
}
}
void AddParagraph(Paragraph para, IEnumerable children) {
AddBlock(para);
try {
inlineCollection = para.Inlines;
foreach (var child in children)
AddDocumentationElement(child);
FlushAddedText(false);
} finally {
inlineCollection = null;
}
}
void AddSpan(Span span, IEnumerable children) {
AddInline(span);
var oldInlineCollection = inlineCollection;
try {
inlineCollection = span.Inlines;
foreach (var child in children)
AddDocumentationElement(child);
FlushAddedText(false);
} finally {
inlineCollection = oldInlineCollection;
}
}
public void AddInline(Inline inline) {
FlushAddedText(false);
if (inlineCollection == null) {
var para = new Paragraph();
para.Margin = new Thickness(0, 0, 0, 5);
inlineCollection = para.Inlines;
AddBlock(para);
}
inlineCollection.Add(inline);
ignoreWhitespace = false;
}
public void AddBlock(Block block) {
FlushAddedText(true);
blockCollection.Add(block);
}
StringBuilder addedText = new StringBuilder();
bool ignoreWhitespace;
public void AddLineBreak() {
TrimEndOfAddedText();
addedText.AppendLine();
ignoreWhitespace = true;
}
public void AddText(string textContent) {
if (string.IsNullOrEmpty(textContent))
return;
for (int i = 0; i < textContent.Length; i++) {
char c = textContent[i];
if (c == '\n' && IsEmptyLineBefore(textContent, i)) {
AddLineBreak(); // empty line -> line break
} else if (char.IsWhiteSpace(c)) {
// any whitespace sequence gets converted to a single space (like HTML)
if (!ignoreWhitespace) {
addedText.Append(' ');
ignoreWhitespace = true;
}
} else {
addedText.Append(c);
ignoreWhitespace = false;
}
}
}
bool IsEmptyLineBefore(string text, int i) {
// Skip previous whitespace
do {
i--;
} while (i >= 0 && (text[i] == ' ' || text[i] == '\r'));
// Check if previous non-whitespace char is \n
return i >= 0 && text[i] == '\n';
}
void TrimEndOfAddedText() {
while (addedText.Length > 0 && addedText[addedText.Length - 1] == ' ') {
addedText.Length--;
}
}
void FlushAddedText(bool trim) {
if (trim) // trim end of current text element
TrimEndOfAddedText();
if (addedText.Length == 0)
return;
string text = addedText.ToString();
addedText.Length = 0;
AddInline(new Run(text));
ignoreWhitespace = trim; // trim start of next text element
}
}
}