(*---------------------------------------- HTMLのテキストファイル編集ユニット 00/05/17 リンクの種類によってMakeLinkListの返す値を変更しました 00/07/31 MakeImgLinkのコードを追加しました。 00/08/03 MakeImgListをBackgroundで指定された画像を列挙するように改造しました。 AddIndexhtml関数を追加しました。 StringAddReplaceIgnoreAllの説明をわかりやすくしました。 00/08/04 PutSlashMark/RemoveSlashMarkを変更 00/08/08 MakeImgListの実装を完全に変更 拡張子を含むテキスト検索で取得する方法に変えました それに伴いAnsiPosRange関数なども含めました。 2011/06/24(金) ・ TagDeleteを改良しTagReplaceも作成しました。 //----------------------------------------*) unit HtmlTxtCtrl; interface uses SysUtils, Classes, Dialogs, Math, StringUnit, StringSearchUnit, uses_end; type TTagRemainFlag = (trPerfect, trRemainCRLF, trTagCRLF); {TagRemainで使う trPerfect 完全にタグだけを残す(タグ外はCRLFもなくなる) trRemainCRLF タグ外のCRLFは削除しない trTagCRLF 終タグの後に常にCRLFを代入} TLinkList = (llFull, llExternal, llInternal, llMail); {MakeLinkListで使う llFull すべてのリンクを列挙する llExternal 外部リンクを列挙する llInternal 内部リンクを列挙する llMail メイルリンクを列挙する} TImgList = (ilFull, ilExternal, ilInternal); {MakeImgListで使う ilFuil すべてのリンクを列挙する ilExternal 外部リンクを列挙する ilInternal 内部リンクを列挙する} procedure MakeLinkList(const HtmlText: String; LinkList: TStrings; LinkListType: TLinkList); //・HTMLファイルのリンクリストを作ります。 // A HREFで指定されたURLのリストをTStringsに掘り込みます procedure MakeImgList(const HtmlText: String; LinkList: TStrings; ImgListType: TImgList); //・HTMLファイルのイメージリンクリストを作ります function HTMLTagDelete(const HtmlText: String): String; //・HTMLテキストからタグ<>で囲まれた部分を削除します function TagDelete(const S, StartTag, EndTag: String; CaseCompare: TCaseCompare = ccIgnoreCase): String; //・テキストから任意のタグで囲まれた部分を削除 function TagReplace(const S, StartTag, EndTag, ReplaceString: String; CaseCompare: TCaseCompare = ccIgnoreCase): String; function HTMLTagRemain(const HtmlText: String; TagRemainFlag: TTagRemainFlag): String; //・HTMLテキストからタグ<>で囲まれた以外の部分を削除します function TagRemain(const S, StartTag, EndTag: String; TagRemainFlag: TTagRemainFlag): String; //・テキストから任意のタグで囲まれた以外の部分を削除 function ExtractAbsoluteURL(const BaseURL, RelativeURL: string): string; //URLの相対アドレスから絶対アドレスを求める function AddIndexhtml(URL: String): String; //フォルダ名URLなどにindex.htmlをつける implementation //////////////////////////////////////////////////////////// //ライブラリ内部共通関数 //////////////////////////////////////////////////////////// //Sの中にSubStrがいくつあるかカウントする //大文字小文字は区別する //function StrCount(const Substr, S: string): Cardinal; //var // Str: String; //begin // Result := 0; // if (Substr = '') or (S = '') then // begin // raise Exception.Create('カウント文字列不正エラー'); // end; // // Str := S; // while AnsiPos( Substr, Str) <> 0 do // begin // Inc(Result); // delete(Str, AnsiPos(Substr, Str), Length(Substr)); // end; //end; //文字列の最後が'/'or'\'ならそれを取り除く function RemoveSlashMark(const S: TFileName): TFileName; begin Result := S; if Length(Result) = LastDelimiter('\/', Result) then SetLength(Result, Length(Result)-1); end; //文字列の最後が'\'なら'/'に置き換え //それでも文字列の最後が'/'じゃないなら追加 function PutSlashMark(const S: TFileName): TFileName; begin Result := RemoveSlashMark(S); Result := Result + '/'; end; //AnsiPosの大小文字区別無し版 //function AnsiIgnoreCasePos(const Substr, S: string): Integer; //begin // Result := AnsiPos( AnsiUpperCase(Substr), AnsiUpperCase(S)); //end; //CRLF以外のものをDeleteする //Deleteの範囲にCRLFが含まれている場合CRLFを後から追加 //CRのみLFのみには対応しない procedure DeleteExceptCRLF(var s : String; index, count : Integer); var i, CRLFCount: Integer; begin CRLFCount := StringCount(#13#10, copy(s, index, count)); Delete( S, index, count); for i := 0 to CRLFCount - 1 do begin Insert( #13#10, s, index); end; end; //------------------------------- //特殊な置き換え関数 { 文字列を追加して置き換える AAAという文字を検索してAAAxxxという値を代入します 検索に大文字小文字は区別しません StringReplaceと何が違うかというと Result := StringReplace( Result, EndTag, EndTag+#13#10, [rfReplaceAll, rfIgnoreCase]); Result := StringAddReplaceIgnoreAll( Result, EndTag, #13#10); この↑のやり方ではEndTagの内容が 大小文字が合わないのでStringAddReplaceEgnoreAllでは独自コーディングしています。 } // function StringAddReplaceIgnoreAll(const S, SearchPattern, AddPattern: String): String; var i, LoopCount, FindIndex: Integer; Str: String; begin Str := S; Result := ''; LoopCount := StringCount( SearchPattern, S, scfIncSubStr, ccIgnoreCase); for i := 0 to LoopCount - 1 do begin FindIndex := PosForward(SearchPattern, Str, ccIgnoreCase); Insert(AddPattern, Str, FindIndex + Length(SearchPattern)); Result := Result + copy(Str, 1, FindIndex + Length(SearchPattern)-1 + Length(AddPattern)-1); Delete(Str, 1, FindIndex + Length(SearchPattern)-1 + Length(AddPattern)-1); end; Result := Result + Str end; //type TPosFunction = function(const Substr, S: string): Integer; //{typeはimplementation内で突然宣言してもOKだよ} // ////------------------------------- ////指定範囲検索基本関数...これは直接使わない //function AnsiPosRangeBase(SearchFunc: TPosFunction; // const SubStr, S: String; StartIndex, EndIndex: Integer): Integer; //var // Str: String; // FindIndex: Integer; //begin // Result := 0; // if (Substr = '') or (S = '') then exit; // if not (StartIndex <= EndIndex) then exit; // {↑↓見事なエラー処理だ} // // if (StartIndex < 1) then exit; // if (Length(S) < EndIndex) then EndIndex := Length(S); // //{EndIndexだけはエラー処理を甘くしてみた。厳しくするなら↓のようにどうぞ} //// if (StartIndex < 1) or (Length(S) < EndIndex) then exit; // // Str := copy(S, StartIndex, EndIndex - StartIndex + 1); // // FindIndex := SearchFunc( SubStr, Str); // if FindIndex = 0 then // Result := 0 // else // Result := StartIndex + FindIndex - 1; //end; // ////------------------------------- ////指定範囲を検索するAnsiPos //function AnsiPosRange(const SubStr, S: String; // StartIndex, EndIndex: Integer): Integer; //begin // Result := AnsiPosRangeBase(AnsiPos, SubStr, S, StartIndex, EndIndex); //end; // ////------------------------------- ////指定範囲を検索するBackAnsiPos //function BackAnsiPosRange(const SubStr, S: String; // StartIndex, EndIndex: Integer): Integer; //begin // Result := AnsiPosRangeBase(BackAnsiPos, SubStr, S, StartIndex, EndIndex); //end; //////////////////////////////////////////////////////////// //外部公開関数 //////////////////////////////////////////////////////////// //------------------------------- //HTMLテキストからリンクリストを返す関数 //LinkListは関数外で生成しておくこと //返す文字列は『URL|リンク名』の形式 procedure MakeLinkList(const HtmlText: String; LinkList: TStrings; LinkListType: TLinkList); //------------------------------- //タグで指定された中身を取得する関数 {タグが複数あっても先頭のFrontTag,BackTag一つしか認識しない タグの順番が逆とか片方が片方に含まれる場合は対応外 "ABCD"などの文字列で前後タグが同じ文字列でもOK} function GetTagText(Body, StartTag, EndTag: String): String; begin Result := Body; if (PosForward( StartTag, Result, ccIgnoreCase) = 0) or (PosForward( EndTag, Result, ccIgnoreCase) = 0) then begin Result := ''; exit; end; {前方/後方タグどちらかが無い場合は''を戻す} Delete( Result, 1, PosForward(StartTag, Result, ccIgnoreCase) - 1 + Length(StartTag)); //前半文字を削除|先頭タグも削除 Result := copy( Result, 1, PosForward(EndTag, Result, ccIgnoreCase) -1); //タグの前にある部分を取得|後方タグも含まない end; //LinkListにmailtoが入っていたら削除 procedure MailToLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin if (PosForward('mailto:', LinkList.Strings[i], ccIgnoreCase) <> 0) or (LinkList[i] = '') then LinkList.Delete(i); end; end; //LinkListにmailtoが入っていなかったら削除 procedure MailToOtherLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin if (PosForward('mailto:', LinkList.Strings[i], ccIgnoreCase) = 0) then LinkList.Delete(i); end; end; //http://が入っていたら削除 procedure HttpLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin if (PosForward('http://', LinkList.Strings[i], ccIgnoreCase) <> 0) or (LinkList[i] = '') then LinkList.Delete(i); end; end; //http://が入っていなかったら削除 procedure HttpOtherLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin if (PosForward('http://', LinkList.Strings[i], ccIgnoreCase) = 0) then LinkList.Delete(i); end; end; //JumpLink#の指定を削除、ジャンプリンクのみのリンクなら行も削除 procedure JumpLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin //URL#top|リンクという書式であるのか if PosForward('#', LinkList[i], ccIgnoreCase) <> 0 then begin //#より左の部分がヌルかどうか // if DelimiterLeft('#', LinkList[i]) = '' then if FirstString(LinkList[I], '#') = '' then LinkList.Delete(i) else // LinkList[i] := // DelimiterLeft('#', LinkList[i]) // + '|' + // DelimiterRight('|', LinkList[i]); LinkList[I] := FirstString(LinkList[I], '#') + '|' + LastString(LinkList[I], '|') end; end; end; var i, LoopCount: Integer; Str: String; SList: TStringList; begin Str := StringReplace(HtmlText, #13, '', [rfReplaceAll]); Str := StringReplace( Str, #10, '', [rfReplaceAll]); //改行をすべて削除 Str := StringReplace(Str, '', ''#13#10, [rfReplaceAll, rfIgnoreCase]); //''の後ろで改行 Str := StringReplace(Str, ' 0 then Str := Str + SList[i] +#13#10; end; finally SList.Free; end; LinkList.Text := Str; //この時点でテキストはで終わる行ばかりになっている //※ for i := 0 to LinkList.Count - 1 do begin LinkList.Strings[i] := GetTagText(LinkList.Strings[i], '"', '"') +'|'+ HtmlTagDelete( LinkList.Strings[i]); end; //タグの中身を切り出す //『URL』|『リンク名』という構成になる //LinkListTypeによって処理分け case LinkListType of llFull: begin end; llExternal: begin JumpLinkDelete; MailToLinkDelete; HttpOtherLinkDelete; end; llInternal: begin JumpLinkDelete; MailToLinkDelete; HttpLinkDelete; end; llMail: begin MailToOtherLinkDelete; end; end; {※ TagRemain(StringReplace( Memo1.Text, #13#10, '', [rfReplaceAll]), '', trTagCRLF); 実はこのような処理でも で囲まれた行だけが残る事になるので 上の※で示されているまでの処理はこの一行ですんでしまう。 たぶんTagRemainを使う方が高速だし。 まあ、好きにしてくれ } end; //------------------------------- //HTMLソースから記述されている画像ファイルを取得する {LinkListは外部で生成しておいてください。 } procedure MakeImgList(const HtmlText: String; LinkList: TStrings; ImgListType: TImgList); //http://が入っていたら削除 procedure HttpLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin if (PosForward('http://', LinkList.Strings[i], ccIgnoreCase) <> 0) or (LinkList[i] = '') then LinkList.Delete(i); end; end; //http://が入っていなかったら削除 procedure HttpOtherLinkDelete; var i: Integer; begin for i := LinkList.Count - 1 downto 0 do begin if (PosForward('http://', LinkList.Strings[i], ccIgnoreCase) = 0) then LinkList.Delete(i); end; end; {↓テキストから'や"を基準に『'xx.gif'』を取り出す  Extnameは'.gif'などの形式} procedure GetImgFilename(Extname, Str: String; StringList1 :TStrings); var ExtIndex, QuotationIndex: Integer; begin ExtIndex := 1; // while AnsiPosRange(AnsiUpperCase(Extname), AnsiUpperCase(Str), // ExtIndex, Length(Str))<>0 do // begin // ExtIndex := AnsiPosRange(AnsiUpperCase(Extname), // AnsiUpperCase(Str), // ExtIndex, Length(Str)); // QuotationIndex := Max( // BackAnsiPosRange('''', Str, 1, ExtIndex), // BackAnsiPosRange('"' , Str, 1, ExtIndex)); {↑↓関数を置き換えた} while PosForward(UpperCase(Extname), UpperCase(Str), ExtIndex, Length(Str))<>0 do begin ExtIndex := PosForward(UpperCase(Extname), UpperCase(Str), ExtIndex, Length(Str)); QuotationIndex := Max( PosBackward('''', Str, 1, ExtIndex), PosBackward('"' , Str, 1, ExtIndex)); if QuotationIndex <> 0 then StringList1.Add(Copy(Str, QuotationIndex+1, (ExtIndex - QuotationIndex)+Length(Extname) - 1 )); Inc(ExtIndex); end; end; var Str: String; SList: TStringList; i, LoopCount: Integer; begin Str := HtmlTagRemain(HtmlText, trPerfect); {↑タグの中身だけを取り出し} SList := TStringList.Create; try GetImgFilename('.gif', Str, SList); GetImgFilename('.jpg', Str, SList); GetImgFilename('.jpeg', Str, SList); GetImgFilename('.png', Str, SList); {↑これらのファイル名だけを画像としてリスト化する} LinkList.Text := SList.Text; finally SList.Free; end; {↑これで『xx.gif』などファイル名だけが残る} LoopCount := LinkList.Count; for i := LoopCount - 1 downto 0 do begin if LinkList.IndexOf(LinkList.Strings[i]) <> i then LinkList.Delete(i); end; {↑ファイル名に重複項目があったら削除します} case ImgListType of {外部リンクと内部リンクかを区別します} ilFull : begin end; ilExternal: begin HttpOtherLinkDelete; end; ilInternal: begin HttpLinkDelete; end; end; end; //////////////////////////////////////////////////////////// //テキストから指定したタグで囲まれた部分を削除します //タグは大小文字区別しません。 //function TagDelete(const S, StartTag, EndTag: String; // CaseCompare: TCaseCompare = ccIgnoreCase): String; //var // i, TagIndex, LoopCount: Integer; // Str: String; //begin // Str := S; // Result := ''; // // try // {↓StartTag,EndTag,互いが互いに重複していないかをチェック} // if ( PosForward( EndTag, StartTag, CaseCompare) <> 0 ) // or ( PosForward( StartTag, EndTag, CaseCompare) <> 0 ) then // raise Exception.Create('タグ不正エラー'); // // LoopCount := StringCount(StartTag, Str, scfIncSubStr, CaseCompare); // // //LoopCountが0でも1〜でも対応している // for i := 0 to LoopCount - 1 do // begin // TagIndex := PosForward( StartTag, Str, CaseCompare); // if TagIndex = 0 then // begin // Str := ''; // break; // end; // Result := Result + copy( Str, 1, TagIndex - 1); // Delete(Str, 1, TagIndex - 1); // // TagIndex := PosForward( EndTag, Str, CaseCompare); // if TagIndex = 0 then // begin // Str := ''; // break; // end; // Delete(Str, 1, TagIndex + Length(EndTag) - 1); // end; //for // Result := Result + Str; // except // Result := ''; // end; //end; function TagDelete(const S, StartTag, EndTag: String; CaseCompare: TCaseCompare = ccIgnoreCase): String; begin Result := TagReplace(S, StartTag, EndTag, '', CaseCompare); end; function TagReplace(const S, StartTag, EndTag, ReplaceString: String; CaseCompare: TCaseCompare = ccIgnoreCase): String; var i, TagIndex, LoopCount: Integer; Str: String; begin Str := S; Result := ''; try {↓StartTag,EndTag,互いが互いに重複していないかをチェック} if ( PosForward( EndTag, StartTag, CaseCompare) <> 0 ) or ( PosForward( StartTag, EndTag, CaseCompare) <> 0 ) then raise Exception.Create('タグ不正エラー'); LoopCount := StringCount(StartTag, Str, scfIncSubStr, CaseCompare); //LoopCountが0でも1〜でも対応している for i := 0 to LoopCount - 1 do begin TagIndex := PosForward( StartTag, Str, CaseCompare); if TagIndex = 0 then begin Str := ''; break; end; Result := Result + copy( Str, 1, TagIndex - 1); Delete(Str, 1, TagIndex - 1); Result := Result + ReplaceString; TagIndex := PosForward( EndTag, Str, CaseCompare); if TagIndex = 0 then begin Str := ''; break; end; Delete(Str, 1, TagIndex + Length(EndTag) - 1); end; //for Result := Result + Str; except Result := ''; end; end; //////////////////////////////////////////////////////////// //HTMLテキストからタグ<>で囲まれた部分を削除します function HTMLTagDelete(const HtmlText: String): String; begin Result := TagDelete( HtmlText, '<', '>'); end; //////////////////////////////////////////////////////////// //テキストから指定したタグで囲まれた部分以外を削除します //タグは大小文字区別しません。 {タグ自体は残ります} function TagRemain(const S, StartTag, EndTag: String; TagRemainFlag: TTagRemainFlag): String; function CRLF(Value: Integer): String; var i: Integer; begin Result := ''; for i := 0 to Value - 1 do begin Result := Result + #13#10; end; end; procedure trPerfectProc; begin end; var i, TagIndex, LoopCount: Integer; Str: String; begin Str := AdjustLineBreaks(S); Result := ''; try if ( PosForward( EndTag, StartTag, ccIgnoreCase) <> 0 ) or ( PosForward( StartTag, EndTag, ccIgnoreCase) <> 0 ) then raise Exception.Create('タグ不正エラー'); LoopCount := StringCount( StartTag, Str, scfIncSubStr, ccIgnoreCase ); case TagRemainFlag of trRemainCRLF: begin //タグに囲まれていない部分を削除 //ただし改行コードは残しておく for i := 0 to LoopCount - 1 do begin TagIndex := PosForward( StartTag, Str, ccIgnoreCase); //ここでtagIndexが0になることはありえない if copy( Str, 1, TagIndex - 1) <> '' then Result := Result + CRLF(StringCount( #13#10, copy( Str, 1, TagIndex - 1), scfIncSubStr, )); Delete( Str, 1, TagIndex - 1); TagIndex := PosForward( EndTag, Str, ccIgnoreCase); if TagIndex = 0 then begin Result := Result + Str; Str := ''; Break; end; Result := Result + copy( Str, 1, TagIndex + Length(EndTag) - 1); Delete( Str, 1, TagIndex + Length(EndTag) - 1); end; DeleteExceptCRLF( Str, 1, Length(Str)); Result := Result + Str; end; trPerfect, trTagCRLF: begin //以下のコードは改行コードも含めてすべてタグに囲まれていない //部分の文字を削除する for i := 0 to LoopCount - 1 do begin TagIndex := PosForward( StartTag, Str, ccIgnoreCase); if TagIndex = 0 then begin break; end; Delete( Str, 1, TagIndex - 1); TagIndex := PosForward( EndTag, Str, ccIgnoreCase); if TagIndex = 0 then begin Result := Result + Str; break; end; Result := Result + copy( Str, 1, TagIndex + Length(EndTag) - 1); Delete( Str, 1, TagIndex + Length(EndTag) - 1); end; end; end; //case case TagRemainFlag of trTagCRLF: begin // Result := StringReplace( Result, EndTag, EndTag+#13#10, [rfReplaceAll, rfIgnoreCase]); //このやり方↑ではEndTagの内容が元の内容と //大小文字が合っていないと変なので修正しました。 Result := StringAddReplaceIgnoreAll( Result, EndTag, #13#10); end; end; except Result := ''; end; end; //////////////////////////////////////////////////////////// //HTMLテキストからタグ<>で囲まれた以外の部分を削除します function HTMLTagRemain(const HtmlText: String; TagRemainFlag: TTagRemainFlag): String; begin Result := TagRemain( HtmlText, '<', '>', TagRemainFlag); end; //////////////////////////////////////////////////////////// //URLの相対アドレスから絶対アドレスを求める function ExtractAbsoluteURL(const BaseURL, RelativeURL: string): string; var i, LoopCount: Integer; begin if (AnsiLowerCase( copy(RelativeURL, 1, 7) ) = 'http://') or (AnsiLowerCase( copy(RelativeURL, 1, 8) ) = 'file:///') then begin Result := RelativeURL; exit; end; if (AnsiLowerCase( copy(BaseURL, 1, 7) ) = 'http://') or (AnsiLowerCase( copy(BaseURL, 1, 8) ) = 'file:///') then begin Result := RemoveSlashMark( BaseURL ); //if AnsiPos('.', DelimiterRight('/', Result)) = 0 then if Pos('.', LastString(Result, '/')) = 0 then //URLの最後にピリオドが含まれていないのなら begin Result := Result + '/a.a'; end; //この時点でhttp://xxx〜xxx/xx.xという書式は保証されたようなもの LoopCount := StringCount('../', RelativeURL, scfIncSubStr); for i := 0 to LoopCount - 1 + 1 do begin // Result := DelimiterLeftLong('/', Result); Result := FirstStringLong(Result, '/'); end; //この時点でhttp://xxx〜xxxとフォルダ名になっているはず if RelativeURL[1] = '/' then Result := Result + RelativeURL else Result := Result + '/' + RelativeURL; Result := StringReplace(Result, '../', '', [rfReplaceAll]); Result := StringReplace(Result, './', '', [rfReplaceAll]); end else begin raise Exception.Create('基本URLがURLとして認識できません'); Result := ''; end; end; //------------------------------- //URLにindex.htmlをつける関数 { http://Delphi.com http://Delphi.com/ http://Delphi.com/index.html → http://Delphi.com/index.html http://Delphi.com/Tips.html → そのまま } function AddIndexhtml(URL: String): String; begin Result := URL; // if AnsiPos('.', DelimiterRight('/', Result)) = 0 then if Pos('.', LastString(Result, '/')) = 0 then {↑URLの最後にピリオドが含まれていないのなら} begin PutSlashMark(URL); Result := Result + 'index.html'; end; end; end.