--追記2017/5/21
こっちにRedmineのプラグイン的な何かを置きました。
RedmineのWikiで表的なまとめを作ろうとするとtextile形式で作ることになるんだけど、ちょっと見た目にこりだしたりすると、とたんにえらいもの書かないといけなくなる。なので、エクセルでまとめた表をぱっと貼り付けられると便利。なんかいいプラグインないかなーと探してみたけど、グッとくるものがいないので、とりあえず作った。
まずはエクセルの表をコピーしてテキストエリアあたりに貼り付け⇒セルのフォーマット情報も含めて内容を取得する。Webサイトで。これにはClipboradApiを使用。下の感じ。
document.querySelector('#pasteArea').addEventListener('paste', function (e) {
console.log(e.clipboardData.getData('text/html'));
});
クリップボードのデータを取得するとき、だいたい”text/plain”で指定するんだけど、htmlで指定すると、セルのフォーマット情報も含んだHTML形式で取得できる。↓の感じ。
で、あとはこのHTMLを解析してTextile形式に変換すれば良いという寸法です。ホントはこの流れをRedmineのプラグインで作りたかったんだけど、Ruby触るの久しぶり過ぎて、すごい時間かかりそうだったので、.netに逃げました。.netでやる場合、HTMLの解析は「HtmlAgilityPack」を使用する。Nugetから取得。
とりあえずで作ったのは、TextAreaにエクセルの表をコピペすると、サーバ側にそのHTMLをテキストで投げて、サーバ側で解析して結果を返す的なAPI。
で困ったことにHTMLをテキストで投げると、.netのセキュリティのチェックにデフォルトで引っかかる。なので、アノテーションでそのチェックを外してあげる。↓の感じ。
[HttpPost]
[ValidateInput(false)]//コレ
public ActionResult Excel2Textile(int id , FormCollection cols)
{
string resultStr = Conv(cols);
return Json(resultStr, JsonRequestBehavior.AllowGet);
}
そしたら、HTMLを解析する。まず、HTMLのテキストをAgilityPackに食わせる。
StringBuilder wkSb = new StringBuilder();
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(excelTableHTML);
で、あとはTableタグ拾って⇒TRタグ拾って⇒TDタグ拾ってを繰り返し。
foreach (var rootnode in htmlDoc.DocumentNode.SelectNodes(@"//table"))
{
//テーブルのTRで回す
foreach (var tmpnode in rootnode.SelectNodes(@"tr"))
{
//TDそれぞれで定義を付ける
foreach (var tmpTdNode in tmpnode.SelectNodes(@"td"))
{
"//table/tr"って書けば外側のループはいらないんだけど理由は後で。これでとりえあず、中身の内容は拾えるんだけど、スタイルはAgilityPackでは解析できないっぽい?なので別途、スタイルシートを解析するライブラリを使う。今回使ったのは「ExCss」。取得はNugetから。これを使ってClass定義とstyle属性の内容を解析する。
今回はそこまで厳密にテーブルを再現したいわけではないので、背景色・文字色・文字寄せ・セル幅くらいを再現する。スタイルの解析は下の感じで書く。
var parser = new Parser();
var stylesheet = parser.Parse(htmlDoc.DocumentNode.SelectSingleNode(@"//style").InnerHtml);
//Class名は.付きにしないと当たらない。ex: .HogeClass
var styleElm = stylesheet.StyleRules.Where(n => n.Value == wkclass).FirstOrDefault();
if (styleElm != null)
{
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
{
if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
break;
}
}
Class定義はこれでいいんだけど、タグ内のStyle属性やる場合、ExCssはクラス定義の形式じゃないと解析できない模様なので、ちょっと文字列足してあげる。↓の感じ。
//クラス名はなんでもよい
var tmppparse = parser.Parse(".hoge{" + tmpTdNode.Attributes["style"].Value.Trim() + "}");
これでだいたい出来上がりなんだけど、問題が一つ。画像とか図形が表にかかってると、マイクロソフトのオレオレ定義が入り込んでくる。↓の感じ。
あと、この定義が入ると、通常のtdタグの中にさらにtableタグが入り込んでくる。てんやわんや。なので、一番表のTableだけで処理したいので、最初のループで最初のtableタグのみ処理するように仕込んでる。
超絶やっつけで作ったやつの全体像は下の感じ。見せられたもんではないのですが、とりあえず。同じロジック2回書いてるし・・・あとで整理しないと。
//ExcelをRedmineのWiki用に変換する
public string ExcelTable2WikiTable(string excelTableDef)
{
StringBuilder wkSb = new StringBuilder();
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(excelTableDef);
string wkInnerText = "";
var parser = new Parser();
var stylesheet = parser.Parse(htmlDoc.DocumentNode.SelectSingleNode(@"//style").InnerHtml);
foreach (var rootnode in htmlDoc.DocumentNode.SelectNodes(@"//table"))
{
//テーブルのTRで回す
foreach (var tmpnode in rootnode.SelectNodes(@"tr"))
{
//TDそれぞれで定義を付ける
foreach (var tmpTdNode in tmpnode.SelectNodes(@"td"))
{
if (tmpTdNode.InnerText.Contains("<!--[if gte vml 1]>"))
{
HtmlDocument innerHtmlDoc = new HtmlDocument();
innerHtmlDoc.LoadHtml(tmpTdNode.OuterHtml);
foreach (var innerNode in innerHtmlDoc.DocumentNode.SelectNodes(@"//table/tr/td"))
{
wkInnerText = innerNode.InnerText;
break;
}
}
else
{
wkInnerText = tmpTdNode.InnerText;
}
Dictionary<string, string> wkDic = new Dictionary<string, string>();
string classsep = "";
wkSb.AppendFormat("|");
//列結合
if (tmpTdNode.Attributes.Where(n => n.Name == "colspan").Count() > 0)
{
wkSb.AppendFormat("\\{0}", tmpTdNode.Attributes["colspan"].Value.Trim());
classsep = ".";
}
//行結合
if (tmpTdNode.Attributes.Where(n => n.Name == "rowspan").Count() > 0)
{
wkSb.AppendFormat("/{0}", tmpTdNode.Attributes["rowspan"].Value.Trim());
classsep = ".";
}
//最初にスタイル属性に入ってる奴を処理
if (tmpTdNode.Attributes.Where(n => n.Name == "style").Count() > 0)
{
var tmppparse = parser.Parse(".hoge{" + tmpTdNode.Attributes["style"].Value.Trim() + "}");
var styleElm = tmppparse.StyleRules.Where(n => n.Value == ".hoge").FirstOrDefault();
if (styleElm != null)
{
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
{
if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
classsep = ".";
break;
}
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "color"))
{
if (!wkDic.ContainsKey("color")) wkDic.Add("color", sTmp.Term.ToString());
classsep = ".";
break;
}
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "width"))
{
if (!wkDic.ContainsKey("width")) wkDic.Add("width", sTmp.Term.ToString());
classsep = ".";
break;
}
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "text-align"))
{
if (!wkDic.ContainsKey("text-align")) wkDic.Add("text-align", sTmp.Term.ToString());
classsep = ".";
break;
}
}
}
//CLASSで指定されてる内容を処理
if (tmpTdNode.Attributes.Where(n => n.Name == "class").Count() > 0)
{
string wkclass = "." + tmpTdNode.Attributes["class"].Value.Trim();
var styleElm = stylesheet.StyleRules.Where(n => n.Value == wkclass).FirstOrDefault();
if (styleElm != null)
{
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "background"))
{
if (!wkDic.ContainsKey("background")) wkDic.Add("background", sTmp.Term.ToString());
classsep = ".";
break;
}
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "color"))
{
if (!wkDic.ContainsKey("color")) wkDic.Add("color", sTmp.Term.ToString());
classsep = ".";
break;
}
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "width"))
{
if (!wkDic.ContainsKey("width")) wkDic.Add("width", sTmp.Term.ToString());
classsep = ".";
break;
}
foreach (var sTmp in styleElm.Declarations.Properties.Where(n => n.Name == "text-align"))
{
if (!wkDic.ContainsKey("text-align")) wkDic.Add("text-align", sTmp.Term.ToString());
classsep = ".";
break;
}
}
}
StringBuilder wkStyleStr = new StringBuilder();
foreach (var tmpkey in wkDic.Keys)
{
wkStyleStr.Append(tmpkey + ":" + wkDic[tmpkey] + ";");
}
if (wkDic.Count > 0) wkSb.Append("{" + wkStyleStr.ToString() + "}");
wkSb.AppendFormat("{0}{1}", classsep, wkInnerText);
}//td
wkSb.Append("|" + Environment.NewLine);
}//tr
break;
}//table
return wkSb.ToString();
}
ちなみに、変換した結果をWikiに貼った結果は下。
けっこーちゃんと出来てる気がする。あー、ちゃんと列・行の結合にも対応出来てたりする。


