--追記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に貼った結果は下。
けっこーちゃんと出来てる気がする。あー、ちゃんと列・行の結合にも対応出来てたりする。