Visual Studio 2019 Tips-NuGetパッケージ化での多言語対応方法
上記でC#のヘルプの多言語化についてまとめたが、ここで作成された情報をもとに、ヘルプドキュメントを生成できないか考えてみた。
前回の方法でVisual Studio上のオブジェクトブラウザでは、使用している言語に合わせてヘルプの情報を参照できるのだが、それ以外の環境では、その情報を見ることができない。
特に今回想定しているのはPowerShellスクリプトユーザー。
一応、PSReadLineでpublicメソッド、プロパティ類は補完できるのだが、それに関するヘルプ情報の提供はできないようだ。
なお補完の例で言うと以下のよう感じ。
この状態でタブキーを押すと、以下の様にnamespace部分まで補完される。
もしくは、オブジェクト生成後、ピリオドでCtrl+Spaceを押すと、以下の様に選択可能なメソッド、プロパティが表示される。
単独のドキュメントの生成としては、C#ソースからであればdoxygenなどのツールが揃えられているのだが、ソースファイルからなので今回の要件には適用できない。
なので、作ることにした。
一部参考にしたのは、以下のサイト。
基本クラス
対象としているのは、nupackされたものをnugetで読み込んだものと考えている。
読み込まれたファイルで利用するのは、アセンブリdllと、XMLヘルプとしてxmlファイル。
これらを読み込み、アセンブリdllの中のアセンブリ情報とXMLヘルプの情報をマージして、マイクロソフトのヘルプみたいなものを出すようにすることをベースとして考えた。
その結果作成した基本クラスが以下のもの。
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Xml;
namespace CommentDoc2Html
{
public class XmlHelpDoc
{
/// <summary>
/// Memberとアセンブリの情報から動作をするためのインターフェース
/// </summary>
public interface IActionMember
{
/// <summary>
/// クラスに対してのアクション<br/>
/// GetConstructors, GetProperties, GetMethodsに対してもdocument.SearchAndActionを実行する
/// </summary>
public void ActionClass(Type refact, Member item, XmlHelpDoc document);
/// <summary>
/// インターフェースに対してのアクション<br/>
/// GetMethodsに対してもdocument.SearchAndActionを実行する
/// </summary>
public void ActionInterface(Type refact, Member item, XmlHelpDoc document);
/// <summary>
/// Enumに対してのアクション<br/>
/// GetEnumNamesに対してもdocument.SearchAndActionを実行する
/// </summary>
public void ActionEnum(Type refact, Member item, XmlHelpDoc document);
public void ActionDelegate(Type refact, Member item, XmlHelpDoc document);
public void Action(MethodInfo refact, Member item, XmlHelpDoc document);
public void Action(ConstructorInfo refact, Member item, XmlHelpDoc document);
public void Action(PropertyInfo refact, Member item, XmlHelpDoc document);
}
/// <summary>
/// MemberのNameを検索するためのパラメータ引数「(...)」というのを作成する
/// </summary>
/// <remarks>実行時、参照先のさらに先のdllの読み込みができないケースがあった。<br/>
/// それに対応するため例外処理を追加している</remarks>
/// <param name="outputParameterName">パラメータ名を出力するかどうか</param>
/// <param name="outputBlankBrackets">パラメータがない場合「()」と出力させる場合true</param>
public static string CreateParameterString(MethodBase refact, bool outputParameterName, bool outputBlankBrackets)
{
try {
var parameters = refact.GetParameters();
if (parameters.Length > 0) {
string result = "(";
bool first = true;
foreach (var para in refact.GetParameters()) {
if (!first) {
result += ",";
}
result += para.ParameterType.FullName.Replace('+', '.');
if (outputParameterName) {
result += " " + para.Name;
}
first = false;
}
result += ")";
return result;
}
}
catch (System.IO.FileNotFoundException) {
// これが発生してしまった場合があったので、
}
if (outputBlankBrackets) {
return "()";
}
else {
return "";
}
}
/// <summary>
/// クラスの名前を作成する<br/>
/// ・ジェネリックに対応している
/// </summary>
public static string CreateClassNameString(Type refact)
{
string className = refact.Name;
if (refact.GetGenericArguments().Length > 0) {
bool first = true;
var back = className.LastIndexOf('`');
if (back >= 0) {
className = className.Remove(back);
}
className += '<';
foreach (var ga in refact.GetGenericArguments()) {
if (!first) {
className += " ,";
}
className += ga.Name;
first = false;
}
className += '>';
}
return className;
}
/// <summary>
/// 指定タイプがデリゲートかどうかの判断
/// </summary>
static public bool IsDelegate(Type refact)
{
if (refact.IsClass) {
for (var parent = refact.BaseType; parent is not null; parent = parent.BaseType) {
if (parent.Name == "Delegate") {
return true;
}
}
}
return false;
}
/// <summary>
/// XMLで読み込んだメンバーに関する情報
/// </summary>
public class Member : IComparable<Member>
{
public string Name = "";
/// <summary>
/// param, typeparam, exception以外のタグで、Depth3に関しての情報を格納する<br/>
/// keyはタグ名
/// </summary>
public Dictionary<string, string> Body = new Dictionary<string, string>();
/// <summary>
/// paramタグの情報を格納する。<br/>
/// keyはnameで指定された文字が設定される
/// </summary>
public Dictionary<string, string> Parameter = new Dictionary<string, string>();
/// <summary>
/// typeparamタグの情報を格納する。<br/>
/// keyはnameで指定された文字が設定される
/// </summary>
public Dictionary<string, string> Typeparam = new Dictionary<string, string>();
/// <summary>
/// exceptionタグの情報を格納する。<br/>
/// keyはcrefで指定された文字が設定される
/// </summary>
public Dictionary<string, string> Exception = new Dictionary<string, string>();
/// <summary>
/// 名前の設定
/// </summary>
public void SetName(string name)
{
var split = name.Split(':');
if (split.Length == 2) {
Name = split[1];
}
}
/// <summary>
/// 比較関数:名前で比較している
/// </summary>
public int CompareTo(Member other)
{
return Name.CompareTo(other.Name);
}
}
/// <summary>
/// XMLドキュメントヘルプを読み込んだもの
/// </summary>
private readonly List<Member> loadedHelp = new List<Member>();
/// <summary>
/// Memberを見つけた際に実行するアクション関数
/// </summary>
public delegate void DoAction(Member member);
/// <summary>
/// 名前を使いメンバーを検索して、存在すればアクションを実行する
/// </summary>
public void SearchAndAction(string name, DoAction func)
{
var result = loadedHelp.BinarySearch(new Member() { Name = name });
if (result >= 0) {
func(loadedHelp[result]);
}
}
/// <summary>
/// XMLヘルプドキュメントを読み込み、後加工しやすいようなMember情報を作成する
/// </summary>
/// <remarks>タグ深さ3のみを処理して取り込むようにしている。<br/>
/// 深さ4は基本brタグだけのみと仮定<br/>
/// 本当はsampleの中のcodeとかもあるかもしれないがそれはそれ</remarks>
/// <param name="xmlDocumentation">XMLヘルプドキュメントファイル名</param>
public void Read(string xmlDocumentation)
{
Member value = null;
ValueTuple<string, string> keyPair = new ValueTuple<string, string>();
using StreamReader sr = new StreamReader(xmlDocumentation); // xml内にdomがあったので、StreamReaderで読み込むようにしてある
using XmlReader xml = XmlReader.Create(sr);
while (xml.Read()) {
switch (xml.NodeType) {
case XmlNodeType.Element:
switch (xml.Name) {
case "member":
value = new Member();
value.SetName(xml["name"]);
break;
case "param":
keyPair.Item1 = xml["name"];
keyPair.Item2 = "";
break;
case "typeparam":
keyPair.Item1 = xml["name"];
keyPair.Item2 = "";
break;
case "exception":
keyPair.Item1 = xml["cref"];
keyPair.Item2 = "";
break;
case "br":
keyPair.Item2 += "\n"; // <br/>を改行コードとして認識させるため
break;
default:
if (xml.Depth == 3) {
keyPair.Item1 = xml.Name;
keyPair.Item2 = "";
}
break;
}
break;
case XmlNodeType.Text:
keyPair.Item2 += xml.Value.Trim(); // 前後のいらない空文字を取り除いておく
break;
case XmlNodeType.EndElement:
if (value is not null) {
switch (xml.Name) {
case "member":
loadedHelp.Add(value);
break;
case "param":
value.Parameter.Add(keyPair.Item1, keyPair.Item2);
break;
case "exception":
value.Exception.Add(keyPair.Item1, keyPair.Item2);
break;
case "typeparam":
value.Typeparam.Add(keyPair.Item1, keyPair.Item2);
break;
default:
if (xml.Depth == 3) {
if (!value.Body.ContainsKey(keyPair.Item1)) {
value.Body.Add(keyPair.Item1, keyPair.Item2);
}
}
break;
}
}
break;
}
}
loadedHelp.Sort();
}
/// <summary>
/// コンストラクタ名に対してのアクション処理
/// </summary>
public void SearchAndAction(string fullName, ConstructorInfo refact, IActionMember action)
{
SearchAndAction(fullName + ".#ctor" + CreateParameterString(refact, false, false), (x) => {
action.Action(refact, x, this);
});
}
/// <summary>
/// プロパティ名に関してのアクション処理
/// </summary>
public void SearchAndAction(string fullName, PropertyInfo refact, IActionMember action)
{
SearchAndAction(fullName + "." + refact.Name, (x) => {
action.Action(refact, x, this);
});
}
/// <summary>
/// メソッド名に関してのアクション処理
/// </summary>
public void SearchAndAction(string fullName, MethodInfo refact, IActionMember action)
{
SearchAndAction(fullName + "." + refact.Name + CreateParameterString(refact, false, false), (x) => {
action.Action(refact, x, this);
});
}
public void SearchAndAction(Type refact, IActionMember action)
{
var fullName = refact.FullName.Replace('+', '.');
SearchAndAction(fullName, (x) => {
if (refact.IsClass) {
if (IsDelegate(refact)) {
action.ActionDelegate(refact, x, this);
}
else {
action.ActionClass(refact, x, this);
}
}
else if (refact.IsInterface) {
action.ActionInterface(refact, x, this);
}
else if (refact.IsEnum) {
action.ActionEnum(refact, x, this);
}
});
}
/// <summary>
/// アセンブリの中でloadedHelp内にヘルプコメントが記載されているものを出力する
/// </summary>
public void Action(Assembly asm, IActionMember action)
{
foreach (var type in asm.GetTypes()) {
SearchAndAction(type, action);
}
}
}
}
利用する側が使用するのは、XmlHelpDoc.IActionMemberを実装したもの。
XmlHelpDocは、XMLヘルプを読み込み、アセンブリ情報とXmlHelpDoc.IActionMemberを指定することで、XMLヘルプとアセンブリ情報の一致するものに対して、IActionMemberのメソッドを呼び出すようにしている。
IActionMember実装部例
HTMLではないが、単純テキストにフォーマットしたものが以下のもの。
修飾子に関する考え方や、delegate判断などについては参考になると思っている。
/// <summary>
/// 平文出力するクラス
/// </summary>
class DocTextWriter : XmlHelpDoc.IActionMember
{
readonly TextWriter Str_;
public DocTextWriter(TextWriter str)
{
Str_ = str;
}
private string ModifierString(MethodBase refact)
{
string value = (refact.IsPublic ? "public" : "") +
(refact.IsVirtual ? " virtual" : "") +
(refact.IsStatic ? " static" : "") +
(refact.IsAbstract ? " abstract" : "") +
(refact.IsFinal ? " final" : "");
return value;
}
private void PrintParameters(XmlHelpDoc.Member item)
{
if (item.Parameter.Count > 0) {
Str_.WriteLine("Parameter:");
foreach (var para in item.Parameter) {
if (para.Value.Length > 0) {
Str_.WriteLine(" {0}", para.Key);
Str_.WriteLine(" {0}", para.Value);
Str_.WriteLine();
}
}
}
}
private void PrintBody(XmlHelpDoc.Member item, string key)
{
if (item.Body.TryGetValue(key, out string value)) {
if (value.Length > 0) {
Str_.WriteLine("{0}:", key);
Str_.WriteLine(" {0}", value);
}
}
}
public void Action(MethodInfo refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
try {
string wName = ModifierString(refact) +
" " + refact.ReturnType.Name +
" " + refact.Name +
" " + XmlHelpDoc.CreateParameterString(refact, true, true) + ";";
Str_.WriteLine("{0}{1}", refact.Name, XmlHelpDoc.CreateParameterString(refact, false, true));
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
Str_.WriteLine(wName);
PrintParameters(item);
PrintBody(item, "returns");
PrintBody(item, "remarks");
}
catch (System.IO.FileNotFoundException) {
// これが発生してしまった場合があったので、
}
}
public void Action(ConstructorInfo refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
try {
string wName = ModifierString(refact) +
" " + refact.ReflectedType.Name +
" " + XmlHelpDoc.CreateParameterString(refact, true, true) + ";";
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
Str_.WriteLine(wName);
PrintParameters(item);
PrintBody(item, "remarks");
}
catch (System.IO.FileNotFoundException) {
// これが発生してしまった場合があったので、
}
}
public void Action(PropertyInfo refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
bool isPublic = false;
string setValue = "";
if (refact.CanWrite) {
if (refact.GetSetMethod() is not null) {
isPublic = true;
}
else {
setValue = "private ";
}
setValue += "set; ";
}
string getValue = "";
if (refact.CanRead) {
if (refact.GetGetMethod() is not null) {
isPublic = true;
}
else {
getValue = "private ";
}
getValue += "get; ";
}
if (!isPublic) {
Str_.WriteLine("{0}", refact.Name);
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
string wName = (isPublic ? "public" : "") +
" " + refact.DeclaringType.Name +
" " + refact.Name;
if (setValue.Length > 0 || getValue.Length > 0) {
wName += "{ " + setValue + getValue + "}";
}
Str_.WriteLine(wName);
PrintBody(item, "remarks");
}
}
public void ActionClass(Type refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
if (refact.Namespace is not null) {
Str_.WriteLine("namespace {0}", refact.Namespace);
}
string className = XmlHelpDoc.CreateClassNameString(refact);
Str_.WriteLine("{0} Class", className);
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
string wName =
(refact.IsPublic || refact.IsNestedPublic ? "public" : "") +
(refact.IsAbstract && refact.IsSealed ? " static" : (refact.IsAbstract ? " abstract" : (refact.IsSealed ? " sealed" : ""))) +
" class " + className;
if (refact.BaseType is not null) {
wName += " : " + refact.BaseType.Name;
}
if (refact.GetInterfaces().Length > 0) {
foreach (var ifname in refact.GetInterfaces()) {
wName += ", " + ifname.Name;
}
}
Str_.WriteLine(wName);
if (refact.GetGenericArguments().Length > 0 && item.Typeparam.Count > 0) {
Str_.WriteLine("Typeparameter:");
foreach (var work in item.Typeparam) {
Str_.WriteLine(" {0}", work.Key);
Str_.WriteLine(" {0}", work.Value);
}
}
PrintBody(item, "remarks");
// コンストラクタ出力
foreach (var work in refact.GetConstructors()) {
document.SearchAndAction(item.Name, work, this);
}
// プロパティ出力
foreach (var work in refact.GetProperties()) {
document.SearchAndAction(item.Name, work, this);
}
// メソッド出力
foreach (var work in refact.GetMethods()) {
document.SearchAndAction(item.Name, work, this);
}
}
public void ActionDelegate(Type refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
var method = refact.GetMethod("Invoke");
if (method is not null) {
try {
string className = XmlHelpDoc.CreateClassNameString(refact);
string wName =
(refact.IsPublic || refact.IsNestedPublic ? "public" : "") +
" delegate " + method.ReturnType.Name + " " +
className + XmlHelpDoc.CreateParameterString(method, true, true) + ";";
if (refact.Namespace is not null) {
Str_.WriteLine("namespace {0}", refact.Namespace);
}
Str_.WriteLine("{0} Delegate", className);
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
Str_.WriteLine(wName);
PrintParameters(item);
PrintBody(item, "returns");
PrintBody(item, "remarks");
}
catch (System.IO.FileNotFoundException) {
}
}
}
public void ActionEnum(Type refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
if (refact.Namespace is not null) {
Str_.WriteLine("namespace {0}", refact.Namespace);
}
Str_.WriteLine("{0} Enum", refact.Name);
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
string wName =
(refact.IsPublic || refact.IsNestedPublic ? "public" : "") +
" enum " + refact.Name;
Str_.WriteLine(wName);
Str_.WriteLine("Fields:");
foreach (var element in refact.GetEnumNames()) {
Str_.Write(" " + element);
document.SearchAndAction(item.Name + "." + element, (x) =>
{
if (x.Body.TryGetValue("summary", out string value)) {
Str_.Write(":" + value);
}
});
Str_.WriteLine();
}
}
public void ActionInterface(Type refact, XmlHelpDoc.Member item, XmlHelpDoc document)
{
if (refact.Namespace is not null) {
Str_.WriteLine("namespace {0}", refact.Namespace);
}
Str_.WriteLine("{0} Interface", refact.Name);
if (item.Body.TryGetValue("summary", out string value)) {
Str_.WriteLine(" {0}", value);
}
string wName =
(refact.IsPublic || refact.IsNestedPublic ? "public" : "") +
" interface " + refact.Name;
Str_.WriteLine(wName);
PrintBody(item, "remarks");
// メソッド出力
foreach (var work in refact.GetMethods()) {
document.SearchAndAction(item.Name, work, this);
}
}
}
メイン部分
上を呼び出しているメイン部分は以下のような感じ。
引数で、nugetパッケージのローディングされた場所を指定している。
static class Program
{
static void Main(string[] args)
{
if (args.Length > 0 && Directory.Exists(args[0])) {
string dllFile = "";
// 引数から渡された場所からdllとxmlを読み込む
foreach (var file in Directory.EnumerateFiles(args[0], "*.dll", SearchOption.AllDirectories)) {
dllFile = file;
}
if (dllFile.Length > 0) {
var asm = Assembly.LoadFile(dllFile);
foreach (var file in Directory.EnumerateFiles(args[0], "*.xml", SearchOption.AllDirectories)) {
var help = new XmlHelpDoc();
help.Read(file);
var writer = new DocTextWriter(Console.Out);
help.Action(asm, writer);
}
}
}
}
}
コメント