(********************************************************************* fountain.txt start 2001/06/05 update 2001/06/09 Copyright (c) 2001 本田勝彦 **********************************************************************) このドキュメントでは、TEditor ver 2.10 で導入された TFountain コンポー ネントと、TFountainParser クラスの仕組みとその拡張について、ソースコード レベルでの記述が行われています。概要についてはヘルプファイルの 「TFountain の導入」「TFountain の拡張」のトピックを参照して下さい。 ソースコードレベルとは言え、内容的には Delphi の基本的な部分についての 記述や、凡長な解説も多々ありますので、heFountain.pas, DelphiFountain. pas, HTMLFountain.pas をご覧になって、その仕組みを理解出来る、あるいは TFountain 拡張コンポを作成出来る方には、あまり有用な内容を含むものではあ りませんので、パスして頂いて構いません。 TFountain の仕組みやメソッドテーブルの考え方は、パーサーを動的に変更出 来る仕組みを模索していた頃、とある方から御紹介頂いた mwEdit という海外製 Delphi 用エディタコンポーネントのソースコードから非常に多くのヒントを得 ていることを、この場を借りてお礼申し上げます。 ---------------------------------------------------------------------- 1.パーサークラスの定義 ---------------------------------------------------------------------- 「パーサークラス」とは、パーサーのクラスです。 「パーサー」とは、パースするオブジェクトです。 「パース」とは、文字列を意味のある語句に切り分けることです。 「文字列」とは、Char(0)..Char(255)(Char($00)..Char($FF) とも書けます) からなる文字の集合体です。Delphi ではこれらの文字を #116 又は16進表記 で #$74 などと記述することが出来ます。 //////////////////////////////////////////////////////////////////////// ・ShowMessage('Hellow!'); と ShowMessage(#$48#$65#$6C#$6C#$6F#$77#$21); は同じ結果を得ます。 ・文字列中の1文字を判別する場合、 var S: String; P: PChar; I: Integer; begin S := 'Hellow!'; for I := 1 to Length(S) do if S[I] = #$77 then hoge; といったことや S := 'Hellow!'; P := PChar(S); while P^ <> #0 do begin if P^ = #119 then hoge; Inc(P); end; といったことが出来ます。 //////////////////////////////////////////////////////////////////////// 「意味のある語句」とは、ある環境において意味を持つひとつの単語です。 ここでは、この語句をトークンと呼びます。 「切り分ける」とは、文字列からトークンを取り出すことです。 「オブジェクト」とは、クラスの実体です。 「クラス」とは、実体を生成する工場です。(批判は覚悟の上で) 「実体」とは、メモリ上に確保されるクラスの機能を実現するために必要な領域、 あるいは、そこへのポインタです。インスタンスとも言います。 「生成する」とは、上記領域をメモリ上に確保すること、あるいは、そこへのポ インタを返す作業です。 つまり「パーサークラス」とは、パーサーの実体を生成する工場です。利用する 工場を選択することによって、生成されるパーサーオブジェクトの性質が変わり、 トークンの切り出され方が変わります。 ---------------------------------------------------------------------- 2.クラス参照型とクラス型 ---------------------------------------------------------------------- 私が初めて Object Pascal 言語リファレンスを読んだ時、最も理解出来なか ったモノのひとつが、このクラス参照でした。正確には「クラス参照型」です。 「クラスを指定する型」ということなのですが、敢えて言えば、上記「工場」を 指定する「型」です。型ですから、その型の「変数」を利用することが出来ます ので、『クラス参照型変数には、「工場」を代入する』ことが出来ます。「クラ スへの参照を代入する」というようにマニュアルには記述されていると思います が、実体ではないクラスを変数に代入することが出来るという概念が、当時の私 には難しかったようです。 対して「クラス型」はクラスの実体を扱う型です。「工場で生成されたモノ」 を代入します。「工場」を代入することは出来ません。 type TEditorClass = class of TEditor; // クラス参照型 TEditorEx = class(TEditor); // TEditor 拡張クラス var EditorClass: TEditorClass; // クラス参照型変数 Editor: TEditor; // クラス型変数 begin EditorClass := TEditorEx; // 工場を代入 Editor := EditorClass.Create(Application); //////////////////////////////////////////////////////////////////////// 出荷時の Delphi は、TEditor も、皆さんが作成されたコンポーネントも知ら ないハズですが、該当コンポをインストールすることで、設計時にちゃんと生成 してくれます。これは、コンポのインストール作業が「Delphi に工場をインス トールする」ことを意味します。Delphi には、クラスの名前と工場を結びつけ るテーブルが用意されていて、フォームを開くときには、.dfm ファイルからク ラスの名前を読み込んで該当工場を検索しその工場に生成してもらうという仕組 みになっています。インストールしていないコンポが貼り付けられたフォームを 開こうとするとエラーが発生するのはこのためです。 //////////////////////////////////////////////////////////////////////// さて、クラス参照型変数には、クラス型と同様、型宣言された時に指定された クラスの下位クラスを代入することが出来ます。TEditor 内部でパーサーを利用 している部分では、 var Parser: TFountainParser; Parser := ActiveFountain.ParserClass.Create(ActiveFountain); というコーディングが行われています。これは、現在利用されている Fountain コンポーネントが指定するパーサーの工場にパーサーを作らせています。 変数 Parser は TFountainParser とその下位クラスのインスタンスを代入出 来る変数ですから、皆さんが作成される TFountainParser の拡張クラスのイン スタンスをすべて受け入れることが出来ます。 TFountain クラスの ParserClass プロパティは、クラス参照型である TFountainParserClass 型の読みとり専用プロパティで、問い合わせに対して、 クラスをつまり工場を返して来ます。この工場は、TFountainParser クラスの下 位クラスでなければなりません。これは、Delphi にインストールするコンポー ネントが TComponent の下位クラスでなければならないのと同じです。逆に言え ば、Delphi は、TComponent の下位クラスであればどんなクラスでもそのインス タンスを生成することが出来るということです。同様に TEditor は、将来皆さ んが作成される TFountainParser の下位クラスのインスタンスを生成して利用 することが出来るのです。 クラス参照万歳! ---------------------------------------------------------------------- 3.NextToken メソッド ---------------------------------------------------------------------- TFountainParser は、文字列からトークンを取り出す作業を行うクラスです。 ですから、その実装はきわめてベタです。べたべたです。 コンストラクタで TFountain コンポーネントの実体を必要とするのは、ヘル プファイルの記述にある通りです。NewData メソッドで受け取った文字列を NextToken メソッドでパースしますが、その仕組みは、Delphi の Classes.pas にある TParser を参考にしています。文字列を1文字づつ判別していって、意 味のあるトークンと思えるところで処理を中断し、その時点でのトークンを返し ます。判別処理を開始した時の最初の文字に対応する判別処理メソッドへ分岐す る仕組みが FMethodTable と、それを初期化する InitMethodTable メソッドで す。 type TFountainParseProc = procedure of object; これは、引数リストの無いオブジェクトのメソッドを扱うための型宣言です。 例えば、 type TForm1 = class(TForm) ........... procedure Button1Click(Sender: TObject); public procedure AruShyori; end; implementation uses heFountain; procedure TForm1.AruShyori; begin ShowMessage('AruShyori called.'); end; procedure TForm1.Button1Click(Sender: TObject); var Proc: TFountainParseProc; begin Proc := AruShyori; Proc; // AruShyori 実行 end; といったことが出来ます。 FMethodTable: array [#0..#255] of TFountainParseProc; FMethodTable は上記 Proc の配列です。その添え字には、Char 型データ #0.. #255 が指定されていますので、総ての Char 型データ(総ての文字)に対して TFountainParseProc 型メソッドを割り当てることが出来ます。 InitMethodTable の実装部では、 for C := #0 to #255 do case C of #0: FMethodTable[C] := EofProc; #9: FMethodTable[C] := TabProc; #10: FMethodTable[C] := LFProc; #13: FMethodTable[C] := CrProc; '0'..'9': FMethodTable[C] := IntegerProc; 'A'..'Z', '_', 'a'..'z': FMethodTable[C] := AnkProc; ............. else FMethodTable[C] := SymbolProc; end; というように、すべての文字に対してメソッドを割り当てています。この状態 で、 var S: String; FP: PChar; begin S := 'procedure TForm1.Button1Click(Sender: TObject);'; // (*1) FP := PChar(S); FMethodTable[FP^]; としたとき、FP^ (FP が指している文字)は 'p' ですから、 FMethodTable['p'] に設定されている AnkProc が実行されます。AnkProc では procedure TFountainParser.AnkProc; // 'A'..'Z', '_', 'a'..'z': begin FToken := toAnk; while FP^ in ['0'..'9', 'A'..'Z', '_', 'a'..'z'] do Inc(FP); end; という具合に、Token プロパティ値を toAnk に設定した後、 FP^ が文字集合 ['0'..'9', 'A'..'Z', '_', 'a'..'z'] に含まれている間ポインタを進めて次 の文字を判別しています。ポインタが procedure の後の半角スペースを指した 時ループを抜けます。 TFountainParser の FP^ を弄り倒しているメソッド群は、以上のような処理 を、他のトークンに対しても行っています。 ---------------------------------------------------------------------- 4.TokenToFountainColor メソッド ---------------------------------------------------------------------- TEditor は、文字列を描画する際、該当文字列をパーサーのインスタンスに渡 してトークンを取得し、さらに、そのトークンをどういう「背景色・文字色・フ ォントスタイル」で描画すれば良いのかをパーサーに問い合わせます。これに応 えるメソッドが TokenToFountainColor メソッドです。ここでは、コンストラク タで受け取った TFountain コンポーネントに保持されている描画情報フィール ドの中で現在のトークンに対応するものを返しています。 TFountainParser は、コンストラクタで受け取った TFountain コンポーネン トのインスタンスを FFountain: TFountain; フィールドデータに保持しておき、 必要に応じて、その Brackets, ReserveWordList プロパティや描画情報フィー ルドを参照します。Brackets, ReserveWordList については TFountain クラス のプロパティなので、そのまま利用出来ますが、TFountain 拡張クラスに実装さ れるプロパティやメソッドへ手を伸ばすためには FFountain フィールドデータ をキャストしてやる必要があります。TDelphiFountainParser では、 function TDelphiFountainParser.TokenToFountainColor: TFountainColor; begin with TDelphiFountain(FFountain) do if IsReserveWord then Result := Reserve else case FToken of toSymbol: Result := FSymbol; toInteger, toFloat: Result := FInt; ............ という具合に FFountain を TDelphiFountain にキャストしています。この下 位クラスへのキャストをダウンキャストと言います。これをやらないと、 FSymbol, FInt といった TDelphiFountain に実装されたフィールドデータへア クセスすることが出来ません。 このダウンキャストは、TDelphiFountainParser のコンストラクタで受け取っ た Fountain が、TDelphiFountain のインスタンスであることが保証されている からこそ可能な技であることに注意して下さい。これは、ヘルプファイルの 「TFountain の拡張」にもある通り、パーサークラスは、自身がどんな TFountain コンポーネントのために存在しているのかを知っていることを意味し ます。TDelphiFountainParser のコンストラクタは、受け取る Fountain が TDelphiFountain であることを信じて疑いませんので、 Parser := TDelphiFountainParser.Create(HTMLFountain1); といったことはしないで下さい。開発時点では、コンストラクタが受け取る Fountain が自身の相手に相応しいかどうかを判別するメソッドの実装も考えた のですが、正しい使い方をする限り必要の無いメソッドを override しなければ ならないのはいかがなモノかということで実装していません。 実装する場合は、例えば type TFountainEx = class(TFountain); TFountainParserEx = class(TFountianParser) protected function RightFountain(AFountain: TFountain): Boolean; virtual; public constructor Create(Fountain: TFountain); override; end; constructor TFountainParserEx.Create(Fountain: TFountain); begin inherited Create(Fountain); if not RightFountain(Fountain) then raise Exception.Create(Fountain.ClassName + ' is not my Fountain'); end; function TFountainParserEx.RightFountain(AFountain: TFountain): Boolean; begin Result := AFountain is TFountainEx; end; といった感じになると思います。 Parser := Editor1.ActiveFountain.ParserClass.Create( Editor1.ActiveFountain); というコーディングスタイルを守る限り RightFountain は必要ありません。 ---------------------------------------------------------------------- 5.文字列データが保持するパース処理に必要なデータについて ---------------------------------------------------------------------- Brackets の各項目と構文要素を表現するデータは、複数行に渡って影響を及 ぼしますが、TEditor の内部文字列オブジェクトである TEditorScreenStrings の基底クラス TRowAttributeStrings はこのデータを保持するフィールドを持っ ています。ファイルを開いた時、文字列に変更があった時、TEditor は該当する 行文字列に対してパースを実行し、その結果を行文字列オブジェクト各行のデー タフィールドに格納しています。その際、ある行をパースし終わった時点での データを、その次の行をパースするためのデータとして格納するという方式をと っています。 WordWrap が False の場合は、Brackets と構文要素についてだけで良いので すが、True の場合、処理は複雑になります。というのも、TEditor の文字列オ ブジェクトは、折り返し表示されたイメージそのままにデータを保持しています ので、ある行の先頭にある語句が前の行末にある語句のつづきである場合もある からです。この処理を行っているのが TFountainParser の LastTokenBracket メソッドです。 //////////////////////////////////////////////////////////////////////// 文字列データをどう保持するかというのは、エディタ設計者を一番悩ませる部 分ではないかと思います。 TEditor のように折り返しイメージそのままの仕様にすると、画面上端に現在 表示されている行番号とリストへのインデックスが一致するので、行番号表示や 描画の処理にかかる時間的なコストを下げることが出来ますし、キャレット位置 から文字列上の行・カラムを求める処理も簡単になる反面、細切れの文字列を パースしなければならないことによる複雑な処理が必要になります。 際限なくメモリを贅沢に使ってしまうことが許されるのであれば、 実際の文字列 1行目 12345678901234567890123456789012345678901234567890 2行目 abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij これを WrapByte 20 で折り返し処理する場合 1.1 12345678901234567890123456789012345678901234567890 1.2 12345678901234567890 1.3 1234567890 2.1 abcdefghijabcdefghijabcdefghijabcdefghijabcdefghij 2.2 abcdefghijabcdefghij 2.3 abcdefghij といったようなデータ構造にして、パースやセーブなどは x.1 を利用すると いった方法も考えられます。 TEditor 開発当初はパースという概念すら持たずに、ひたすら動作するエディ タコンポを目指していましたので、こういう仕様は思いつきませんでした。 486Dx 50MHz という非力なマシンで、描画速度最優先が現在の仕様を生みました。 //////////////////////////////////////////////////////////////////////// さて、折り返し処理によって分断されたトークンの扱いですが、heFountain. pas の LastTokenBracket メソッドにあるコメントを、よりわかりやすい形で述 べたいと思います。 通常、文字列の先頭からパースを開始するにあたって必要と思われるデータは 以下の2つです。 ・Bracets で囲まれた領域かどうかを判別するためのデータ ・求める構文要素の中にあるトークンであるかどうかを判別するためのデータ さらに、前の行末で折り返し処理によって分断されたトークンの場合では ・折り返し位置からトークンの終端までの長さ(下記パターン1の場合は7) ・その時のトークンの種類 ・構文要素を取得するために、そのトークンの直前にあったトークンの種類 といったデータがあれば、決められた長さの文字列を指定されたトークンとし て切り出せば良さそうに思えます。 「折り返されたトークン」には以下の2つのパターンがあります。 パターン1 2行にまたがるトークン +---------------------+ 1 行目 | *******| +---------------------+ 2 行目 |******* | +---------------------+ パターン2 3行以上にまたがるトークン +---------------------+ 1 行目 | *******| +---------------------+ 2 行目 |*********************| +---------------------+ n-1 行目 |*********************| +---------------------+ n 行目 |******* | +---------------------+ ここで2行目の文字列をパースする場合について考ると、パターン1の場合は、 7文字分を指定されたトークンとして切り出すことが可能ですが、パターン2の 場合21文字を一つのトークンとして切り出してしまうと n - 1 行目を新たな トークンとして扱わなければならなくなり、この方式は使えません。 ここからが複雑怪奇劇場の始まりです。 この場合(2行目が raWrapped の場合「*1」)折り返し位置からトークン の終端までの長さを取得する方式を諦め、この値を保持する WrappedBytes フ ィールドには0を格納し、2行目をパースするのは、指定されたトークン専用の パースメソッドを用意することにします。この指定されたトークン専用のパース メソッド配列が FTokenMethodTable です。InitMethodTable メソッドでは、 ............... // FTokenMethodTable for C := #0 to #255 do case C of toSymbol: FTokenMethodTable[C] := SymbolProc; toInteger, toFloat: FTokenMethodTable[C] := IntegerProc; toBracket: FTokenMethodTable[C] := BracketProc; toReserve: FTokenMethodTable[C] := ReserveWordProc; toComment: FTokenMethodTable[C] := CommenterProc; toAnk: FTokenMethodTable[C] := AnkProc; .............. else FTokenMethodTable[C] := SymbolProc; end; という具合に、パースを開始するにあたって、指定されたトークンとしてパー スするためのメソッドテーブルが用意されています。この「指定されたトーク ン」というデータは StartToken データフィールドに格納されます。 次に、WrappedBytes 分ポインタを進める処理では、そこにタブ文字があると ロジックが破綻するという「タブ文字問題」があります。これは、TEditor の描 画メソッドが、タブ文字が展開された文字列受け取ってそれをパースしながら描 画するという仕様に原因があります。これも描画速度を得るため、タブ文字を展 開された文字列を一旦ノーマルの状態で描いてから、それをパースし、必要な部 分だけ色づけして上描きするという仕様によっています。 タブ文字を含む可能性のあるトークン toComment, toSingleQuotation, toDoubleQuotation 「*2」についても WrappedBytes には0を格納し、 FTokenMethodTable に処理を委ねる仕様としています。 さらに(これで終わりです)toBracket もタブ文字を含む場合があります。 NextToken メソッドでは、Brackets プロパティ値によって BracketProc へ処理 を分岐しますので、折り返されたトークンが toBracket の場合も WrappedBytes には0を格納すれば良いことになるのですが、BracketProc では、 該当 RightBracket に指定された文字列が折り返されている場合「*3」 RightBracket を発見することが出来ないという問題があるので、この場合だけ WrappedBytes の仕組みを利用します。 #ユーザーの皆さんが、toBracket, toComment, toSingleQuotatio, toDoubleQuotation 以外で、タブ文字を内包するトークンを取得出来るような パーサーを書いた場合、このシステムは破綻します。 LastTokenBracket メソッドは以上のような処理を行っています。 ---------------------------------------------------------------------- 6.新しいトークンの追加 ---------------------------------------------------------------------- THTMLFountain では、いくつかの新しいトークンが追加されていますが、ここ では toAmpersand を例として取り上げます。 toAnpersand は '&' で始まって 文字集合 ['#', '0'..'9', 'A'..'Z', 'a'.. 'z'] に含まれる文字の連続する文字列(だと思う)ですので、まず、 InitMethodTable を override し、'&' に対応するメソッドを FMethodTable['& '] に登録する作業が必要になります。 inherited InitMethodTable; ............ FMethodTable['&'] := AmpersandProc; これによって、NextToken メソッド実行時に FP^ が '&' の場合、 AmpersandProc が実行されます。その実現部では、 procedure THTMLFountainParser.AmpersandProc; begin FToken := toAmpersand; if not FIsStartToken then Inc(FP); while FP^ in ['#', '0'..'9', 'A'..'Z', 'a'..'z'] do begin Inc(FP); if FP^ = ';' then begin Inc(FP); Break; end; end; end; というように、現在のトークンを toAmpersand に設定し、上記文字集合から なる文字列を返す処理を行っています。 2行目の FIsStartToken がこの章の核心部分です。InitMethodTable メソッ ドでは、FTokenMethodTable['&'] := AmpersandProc; が実行されていることに 注目してください。(HTMLFountain.pas 参照)つまり前章で述べた FTokenMethodTable に処理が委ねられた場合への対応も AmpersandProc で行っ ています。FInStartToken フィールドデータは、NextToken メソッドで FTokenMethodTable に処理が委ねられたことを示すフラグとして値が設定されて います。フラグが真の場合 '&' で始まるトークンが折り返されているわけです から、パースする行文字列の先頭に '&' が存在することはあり得ませんので、 そのための処理が行われています。 この例のように、一つのメソッドを共用することも(トークンの形式によって は)可能ですが、折り返されたトークンの処理を FTokenMethodTable に委ねる 場合があるという仕様に変わりはありませんので、新しいトークンを定義した場 合は、その取得方法のためのメソッドを FMethodTable に追加するか取得するた めの仕組みを実装すると同時に、折り返される可能性のあるトークンの場合は必 ず FTokenMethodTable にも対応しなければなりません。toTagStart などのよう に1文字で構成されるトークンは、折り返されることがあり得ませんので対応の 必要ありません。 ---------------------------------------------------------------------- 7.構文要素を扱う(1) ---------------------------------------------------------------------- Delphi に付属のコードエディタでは read, write, index の語句が property 節の中でだけハイライト表示されています。また、asm, end で囲まれ た複数行に渡る領域を指定色で表示しているにも関わらず asm, end ブロック中 の // コメントや (* *) { } を指定色で表示する機能が実現されています。 この機能を TEditor でも実現するため、構文要素(と私が勝手に呼んでいる だけですが)を扱うために導入されたのが、 TFountainParser の FElementIndex データフィールドです。 構文要素を取得する仕組みは、Brackets に良く似てはいますが、指定された 語句によってだけ囲まれた領域を認識する Brackets よりも、より柔軟な要素を 扱うことが出来ます。TDelphiFountain の AnkProc では、 PropertyBlockElement を解除するかどうかの判別を、end, function, private, procedure, protected, public, published という語句に対して行っています。 property 節内の read, write, index をハイライト表示する処理について ソースコードの流れを追いながら説明したいと思います。 まず property 節内であることを表現する構文要素を表す定数を定義します。 const PropertyBlockElement = 1; 次に、AnkProc を override して、構文要素の値に応じた case 文によって処 理を分岐させます。まず、通常の構文要素内にある時( NormalElementIndex ) は、FP^ が指している文字が P, p の場合、そこから始まる語句が property で あるかどうかを判別し、真の場合は、ポインタをその語句の最後の位置まで進め ( IsKeyWord メソッド参照)FElementIndex を PropertyBlockElement に更新 します。次に AnkProc が実行される時は、PropertyBlockElement ブロックが実 行されます。ここでは、property 節内でだけ有効なキーワードの場合は toReserve トークンを返す。あるいは property 節を終了させる語句の場合は、 FElementIndex を NormalElementIndex で更新するといった処理が行われていま す。 property 節は通常語句 property で始まり、語句 ; で終了しますので、FP^ の文字が ; の場合は、property 節内であることを終了させる処理が必要になり ます。この処理は、PropetyCancelProc で行われています。InitMethodTable 内 では FP^ が ; の場合、この PropertyCancelProc が実行されるように設定され ています。 FMethodTable[';'] := PropertyCancelProc; procedure TDelphiFountainParser.PropertyCancelProc; // ';' begin if FElementIndex = PropertyBlockElement then FElementIndex := NormalElementIndex; SymbolProc; // 普通の記号として扱う end; asm, end ブロックもこの処理に準じます。 ---------------------------------------------------------------------- 8.構文要素を扱う(2)直前のトークンの利用 ---------------------------------------------------------------------- 5章でも少しだけ記述していますが、FElementIndex の他にもう一つのデータ として、直前のトークンを保持しておく FPrevToken データフィールドが用意さ れています。このデータによって、「ある構文要素中の、あるトークンに続く トークン」といったことを認識することが出来ます。THTMLFountain では、この 方式で toTagElement, toTagAttribute といったトークンを取得しています。 THTMLFountainParser では、TagBlockElement を定義し、< で突入 > で解除 という処理を行っています。ここまでは TDelphiFountainParser と同じですが、 タグ中のトークンでは、その出現場所によってトークンの種類を判別する処理が 行われています。この処理は override された NextToken で実行される UpdateTagToken メソッドで行われています。また NextToken メソッドでは現在 のトークンを次回のパースで直前のトークンとして扱うために FPrevToken フ ィールドデータを更新していることにも注意して下さい。 procedure THTMLFountainParser.UpdateTagToken; begin if (FToken <> toEof) and (FElementIndex = TagBlockElement) then // toEof ではないタグ中のトークンの場合 case FPrevToken of // 直前のトークンが toTagStart: // < の直後の toAnk は toTagElement に変更 if FToken = toAnk then FToken := toTagElement; toDoubleQuotation, toTagElement, toTagAttributeValue: // "hoge", toTagElement, toTagAttributeValue の直後のトークンは // toTagAttribute に変更 FToken := toTagAttribute; toTagAttribute: // toTagAttribute 直後の = ではないトークンは toTagAttribute に // 変更 if FToken <> toTagAttributeDelimiter then FToken := toTagAttribute; end; end; また、FP^ が = の時、toTagAttributeDelimiter とするかどうかの判別処理 を行う TagAttributeDelimiterProc そして / の後に続く文字列を toTagElement とするかどうかを判別する SlashProc が InitMethodTable メソ ッドでそれぞれの文字に割り当てられていることを確認して見て下さい。 ---------------------------------------------------------------------- 9.終わりに ---------------------------------------------------------------------- 私は、Delphi しか知りません。HTML, dBASE, Perl であれば多少わかります。 N88-BASIC も入れて良いのだろうか(^^; 様々な言語とその仕様に精通された皆 様の手によって TFountain 拡張コンポが次々と公開されることを希望し、この ドキュメントを作成しました。TFountain リンクのページも用意させて頂きます。 皆様の作品を心よりお待ち致しております。