AsciiDocで結合セルを作るのがややこしい(といってもHTMLと同様だが)ので、AsciiDocに不慣れでも表のひな形を作成できるように、Excelの表からAsciiDocに変換するツールを作ってみた。
Excel用に作ってたらWordも小変更で対応できた。(セルの中身の処理はテキトウにつくったので、過度な期待は禁物。)
スクリーンショット
Excel
上記の出力結果のコピー
|===
.2+|a |b |c |d 2.2+|e
.2+|f |g |h
|i .2+|j |k |l |m
2+|n |o |p |q
|===
Word
上記の出力結果のコピー
|===
| A | B | C
2+| D .2+| E
| F | G
|===
C#ソースコード
- 処理対象は表1個だけです。
- ネストした表(テーブル内にテーブルがある)には対応していません。
- 書式は無視します。
ClipboardedHtmlToAsciiDocTable.cs
usingSystem;usingSystem.Collections.Generic;usingSystem.Drawing;usingSystem.IO;usingSystem.Text;usingSystem.Text.RegularExpressions;usingSystem.Windows.Forms;classSampleForm:Form{staticreadonlyintExpectedHeaderMaxLines=20;staticreadonlyRegexrxTableBeginTag=newRegex(@"<table(?:\s)?[^>]*>",RegexOptions.Multiline|RegexOptions.IgnoreCase);// 1 2 3 *?は最短マッチ o:pはMS office(word)対策staticreadonlyRegexrxTag=newRegex(@"<([a-z][a-z0-9]*|o:p)(|\s[^>]*)>|</([a-z][a-z0-9]*|o:p)>|<!--(?:.*?)-->",RegexOptions.Multiline|RegexOptions.IgnoreCase);//static readonly Regex rxTag = new Regex(@"<([a-z][a-z0-9]*)(|\s[^>]*)>|</([a-z][a-z0-9]*)>|<!--(?:.*?)-->", RegexOptions.Multiline | RegexOptions.IgnoreCase);TextBoxtxtAdoc;SampleForm(){Text="HTML table(Clipborad) to AsciiDoc";ClientSize=newSize(700,430);varbtn=newButton(){Size=newSize(280,25),Text="Get AsciiDoc from Clipborad",};btn.Click+=(s,e)=>{ParseFromHtmlClipboard();};Controls.Add(btn);varbtnDbg=newButton(){Location=newPoint(300,0),Size=newSize(220,25),Text="Get HTML from Clipborad(開発者用)",};btnDbg.Click+=(s,e)=>{DumpHtmlClipboard();};Controls.Add(btnDbg);txtAdoc=newTextBox(){Location=newPoint(0,30),Size=newSize(700,400),Text="",Multiline=true,WordWrap=false,// 折り返し表示をしないScrollBars=ScrollBars.Both,};Controls.Add(txtAdoc);txtAdoc.KeyDown+=(s,e)=>{if(e.Control&&e.KeyCode==Keys.A){((TextBox)s).SelectAll();}};Resize+=(s,e)=>{MyResize();};ResizeEnd+=(s,e)=>{MyResize();};}voidMyResize(){inth=ClientSize.Height-txtAdoc.Top;if(h<50){h=50;}txtAdoc.Size=newSize(ClientSize.Width,h);}voidParseFromHtmlClipboard(){MemoryStreamms=GetHtmlClipboard();if(ms!=null){stringtmp=Parse(ms);if(tmp!=null){txtAdoc.Text=tmp;txtAdoc.Focus();txtAdoc.SelectAll();}else{txtAdoc.Text="Parse Failed";}}else{txtAdoc.Text="Clipboard Load failed";}}voidDumpHtmlClipboard(){MemoryStreamms=GetHtmlClipboard();if(ms!=null){stringtmp=GetHtmlText(ms);if(tmp!=null){txtAdoc.Text=tmp;//txtAdoc.Focus();//txtAdoc.SelectAll();}else{txtAdoc.Text="Parse Failed";}}else{txtAdoc.Text="Clipboard Load failed";}}staticMemoryStreamGetHtmlClipboard(){returnClipboard.GetData("Html Format")asMemoryStream;}staticstringGetHtmlText(MemoryStreamms){intstartHtml=-1;intendHtml=-1;// ヘッダ情報(StartHTML, EndHTML)を取得// StartHTML:nnnnnnnnnn// EndHTML:nnnnnnnnnn//public StreamReader(Stream stream, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize, bool leaveOpen)// leaveOpen=trueで開かないと、msが閉じてしまう。using(varsr=newStreamReader(ms,Encoding.UTF8,true,1024,true)){Regexrx=newRegex(@"^(StartHTML:|EndHTML:)([0-9]+)");intlineCount=0;strings;while((s=sr.ReadLine())!=null){lineCount++;Matchm=rx.Match(s);if(m.Success){intn=Convert.ToInt32(m.Groups[2].Value,10);// 10進if(m.Groups[1].Value=="StartHTML:"){startHtml=n;}else{endHtml=n;}if(startHtml>=0&&endHtml>startHtml){break;}}if(lineCount>=ExpectedHeaderMaxLines){break;}}}// HTML部分を取得(EndHTMLは無視)ms.Position=startHtml;using(varsr=newStreamReader(ms,Encoding.UTF8,false)){returnsr.ReadToEnd();}}// stypeタグの中身を返すstaticstringGetStyleText(stringhtmlText,outintendPos){endPos=0;intstyleStartTagPos=htmlText.IndexOf("<style>");if(styleStartTagPos<0){returnnull;}styleStartTagPos+="<style>".Length;intstyleEndTagPos=htmlText.IndexOf("</style>",styleStartTagPos);if(styleEndTagPos<0){returnnull;}endPos=styleEndTagPos+"</style>".Length;intcommentStartTagPos=htmlText.IndexOf("<!--",styleStartTagPos);if(commentStartTagPos>=0){commentStartTagPos+="<!--".Length;}intcommentEndTagPos=(commentStartTagPos<0)?-1:htmlText.IndexOf("-->",commentStartTagPos);if(commentStartTagPos>=0&&commentEndTagPos>commentStartTagPos&&commentEndTagPos<styleEndTagPos){// コメントタグがある場合、コメントタグを除去(コメントタグ内のみを返す)returnhtmlText.Substring(commentStartTagPos,commentEndTagPos-commentStartTagPos);}else{// コメントタグがない場合returnhtmlText.Substring(styleStartTagPos,styleEndTagPos-styleStartTagPos);}}staticintIndexOfUsingRegex(stringsrc,intstartPos,RegexrTarget,outintlength){Matchm=rTarget.Match(src,startPos);if(!m.Success){length=0;return-1;}length=m.Groups[0].Length;returnm.Groups[0].Index;}// tableタグ込みで返す// ネストは許容しない(検出してnullを返す)staticstringGetFirstTableText(stringhtmlText,intpos){intlen;inttableStartTagPos=IndexOfUsingRegex(htmlText,pos,rxTableBeginTag,outlen);if(tableStartTagPos<0){returnnull;}inttableEndTagPos=htmlText.IndexOf("</table>",tableStartTagPos+len);if(tableEndTagPos<0){returnnull;}intdummy;inttmpPos=IndexOfUsingRegex(htmlText,tableStartTagPos+len,rxTableBeginTag,outdummy);if(tmpPos>=0&&tmpPos<tableEndTagPos){// ネストしている(閉じタグよりも手前の位置に2つ目の開始タグを検出した)returnnull;}tableEndTagPos+="</table>".Length;returnhtmlText.Substring(tableStartTagPos,tableEndTagPos-tableStartTagPos);}//static readonly Regex rxCss = new Regex(@":;", RegexOptions.Multiline | RegexOptions.IgnoreCase);//static Dictionary<string,Dictionary<string,string>> ParseCssPart(string styleText)//{//}// https://momdo.github.io/html/syntax.html#attributes-2// 属性名は、制御文字、U+0020 SPACE、U+0022(")、U+0027(')、U+003E(>)、U+002F(/)、U+003D(=)、および非文字以外の1つ以上の文字で構成されなければならない。HTML構文において、外来要素に対するものでさえ、属性名は、ASCII小文字およびASCII大文字の任意の組み合わせで書かれてもよい。// 属性値は、テキストが曖昧なアンパサンドを含めることができない追加の制限をもつ場合を除き、テキストおよび文字参照の混合物である。// 引用符で囲まれない属性値構文// ASCII空白文字 U+0022 QUOTATION MARK文字(")、// U+0027 APOSTROPHE文字(')、U+003D EQUALS SIGN文字(=)、// U+003C LESS-THAN SIGN文字(<)、U+003E GREATER-THAN SIGN文字(>)、// またはU+0060 GRAVE ACCENT文字(`)文字を含んではならず、かつ空文字列であってはならない。// 1 = 2 3 4 // <-------------------------------------> <--------------------------------> <-----> <------->// <---------------------------------------------------------------->staticreadonlyRegexrxAttr=newRegex(@"\b([^\x00-\x1F\x20\x22\x27\x2F\x3D\x3E]+)\s*(?:=\s*(?:([^\x20\x22\x27\x3C\x3D\x3E\x60]+)|'([^']*)'|\x22([^x22]*)\x22))?",RegexOptions.Multiline|RegexOptions.IgnoreCase);staticDictionary<string,string>ParseAttrs(stringattrsStr){vardict=newDictionary<string,string>();MatchmAttr=rxAttr.Match(attrsStr);while(mAttr.Success){stringkey=mAttr.Groups[1].Value.ToLower();stringvalue="";if(mAttr.Groups[2].Length>0){value=mAttr.Groups[2].Value;}elseif(mAttr.Groups[3].Length>0){value=mAttr.Groups[3].Value;}elseif(mAttr.Groups[4].Length>0){value=mAttr.Groups[4].Value;}else{// without "="// do nothing}if(!dict.ContainsKey(key)){dict.Add(key,value);}mAttr=mAttr.NextMatch();}returndict;}staticstringEscapeContentForAdocTableCell(strings){// Replace (string input, string replacement);s=rxTag.Replace(s,"");// HTML全般のタグを消去s=s.Replace("\r\n"," ").Replace("\n"," ").Replace("\r"," ").Replace("\t"," ").Replace(" "," ").Replace("<","<").Replace(">",">").Replace("&","&").Replace("|","{VBar}");// ADoc用returns;}staticstringParseTableToAdoc(stringtableText){varsb=newStringBuilder();Matchm=rxTag.Match(tableText);stringlastStartTag=null;intlastPos=-1;intlastTdPos=-1;intcurrentTdCount=0;sb.AppendLine("|===");while(m.Success){if(m.Groups[1].Length>0){stringtag=m.Groups[1].Value;stringattrsStr=m.Groups[2].Value;lastPos=m.Groups[0].Index+m.Groups[0].Length;if(tag=="tr"){currentTdCount=0;}elseif(tag=="td"){lastTdPos=lastPos;if(lastStartTag!="tr"){sb.Append(" ");}varattrs=ParseAttrs(attrsStr);if(attrs.ContainsKey("colspan")||attrs.ContainsKey("rowspan")){if(attrs.ContainsKey("colspan")){sb.Append(attrs["colspan"]);}if(attrs.ContainsKey("rowspan")){sb.Append(".");sb.Append(attrs["rowspan"]);}sb.Append("+");}sb.Append("|");currentTdCount++;}lastStartTag=tag;}elseif(m.Groups[3].Length>0){stringtag=m.Groups[3].Value;inttagStartPos=m.Groups[0].Index;//Console.WriteLine("</" + m.Groups[3].Value +">");if(tag=="tr"){sb.AppendLine("");}elseif(tag=="td"){strings=tableText.Substring(lastTdPos,tagStartPos-lastTdPos);sb.Append(EscapeContentForAdocTableCell(s));lastTdPos=-1;}lastPos=-1;lastStartTag=null;}m=m.NextMatch();}sb.AppendLine("|===");Console.WriteLine(sb.ToString());returnsb.ToString();}staticstringParse(MemoryStreamms){// debug code {//string htmlText = File.ReadAllText("testdata_html_excel.txt");// } end of debug codestringhtmlText=GetHtmlText(ms);intpos;stringstyleText=GetStyleText(htmlText,outpos)??"";stringtableText=GetFirstTableText(htmlText,pos);returnParseTableToAdoc(tableText);}[STAThread]staticvoidMain(string[]args){Application.Run(newSampleForm());}}
JavaScriptに移植してWeb上に公開してみた
開発時メモ: クリップボード形式は MIMEタイプとしてtext/html
を指定すると、オフセット情報とかのヘッダ情報なしの html が得られる。
ネストチェックはしていない。
See the Pen TableOfHtml2AsciiDoc by kob58im (@kob58im) on CodePen.
TableOfHtml2AsciiDoc - CodePen