{-----------------------------------------------------------------------------
The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/MPL-1.1.html

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for
the specific language governing rights and limitations under the License.

The Original Code is: JvXMLTree.PAS, released on 2002-06-15.

The Initial Developer of the Original Code is Jan Verhoeven [jan1 dott verhoeven att wxs dott nl]
Portions created by Jan Verhoeven are Copyright (C) 2002 Jan Verhoeven.
All Rights Reserved.

Contributor(s): Robert Love [rlove att slcdug dott org].

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.delphi-jedi.org

Known Issues:
-----------------------------------------------------------------------------}
// $Id$

unit JvXmlTree;

{$I jvcl.inc}

interface

uses
  {$IFDEF UNITVERSIONING}
  JclUnitVersioning,
  {$ENDIF UNITVERSIONING}
  SysUtils, Classes, Variants,
  JvStrings;

type
  TJvXMLValueType = (xvtString, xvtCDATA);
  TJvXMLFilterOperator = (xfoNOP, xfoEQ, xfoIEQ, xfoNE, xfoINE, xfoGE,
    xfoIGE, xfoLE, xfoILE, xfoGT, xfoIGT, xfoLT, xfoILT);

  TJvXMLTree = class;

  TJvXMLFilterAtom = class(TObject)
  private
    FValue: string;
    FName: string;
    FOperator: TJvXMLFilterOperator;
    FAttributeFilter: Boolean;
  public
    property Name: string read FName write FName;
    property Operator: TJvXMLFilterOperator read FOperator write FOperator;
    property Value: string read FValue write FValue;
    property AttributeFilter: Boolean read FAttributeFilter write FAttributeFilter;
  end;

  TJvXMLFilter = class(TObject)
  private
    FName: string;
    FFilters: TList;
    procedure Initialize(FilterStr: string);
  public
    constructor Create(const FilterStr: string);
    destructor Destroy; override;
    property Name: string read FName write FName;
    property Filters: TList read FFilters write FFilters;
  end;

  TJvXMLNode = class;

  TJvXMLAttribute = class(TObject)
  private
    FName: string;
    FValue: Variant;
    FParent: TJvXMLNode;
  public
    constructor Create(AParent: TJvXMLNode; const AName: string; AValue: Variant);
    function Document: string;
    property Name: string read FName write FName;
    property Value: Variant read FValue write FValue;
    property Parent: TJvXMLNode read FParent write FParent;
  end;

  TJvXMLNode = class(TObject)
  private
    FName: string;
    FValue: Variant;
    FNodes: TList;
    FAttributes: TList;
    FParentNode: TJvXMLNode;
    FValueType: TJvXMLValueType;
  public
    constructor Create(const AName: string; AValue: Variant; AParent: TJvXMLNode);
    destructor Destroy; override;
    // added 29-July-2000
    function GetNamePathNode(const APath: string): TJvXMLNode;
    procedure DeleteNamePathNode(const APath: string);
    function ForceNamePathNode(const APath: string): TJvXMLNode;
    function GetNamePathNodeAttribute(const APath, AName: string): TJvXMLAttribute;
    procedure DeleteNamePathNodeAttribute(const APath, AName: string);
    function ForceNamePathNodeAttribute(const APath, AName: string; AValue: Variant): TJvXMLAttribute;
    function AddNode(const AName: string; AValue: Variant): TJvXMLNode;
    function AddNodeEx(const AName: string; AValue: Variant): TJvXMLNode;
    procedure DeleteNode(Index: Integer);
    procedure ClearNodes;
    function AddAttribute(const AName: string; AValue: Variant): TJvXMLAttribute;
    function GetAttributeValue(const AName: string): Variant;
    procedure DeleteAttribute(Index: Integer);
    procedure ClearAttributes;
    function Document(ALevel: Integer): string;
    function GetNodePath: string;
    function GetNamedNode(const AName: string): TJvXMLNode;
    function SelectSingleNode(const APattern: string): TJvXMLNode;
    procedure SelectNodes(APattern: string; AList: TList);
    function TransformNode(AStyleSheet: TJvXMLNode): string;
    function Process(ALevel: Integer; ANode: TJvXMLNode): string;
    function FindNamedNode(const AName: string): TJvXMLNode;
    procedure FindNamedNodes(const AName: string; AList: TList);
    procedure GetAllNodes(AList: TList);
    function GetNamedAttribute(const AName: string): TJvXMLAttribute;
    procedure FindNamedAttributes(const AName: string; AList: TList);
    function MatchFilter(AObjFilter: TJvXMLFilter): Boolean;
    procedure MatchPattern(const APattern: string; AList: TList);
    procedure GetNodeNames(AList: TStrings);
    procedure GetAttributeNames(AList: TStrings);
    function GetNameSpace: string;
    function HasChildNodes: Boolean;
    function CloneNode: TJvXMLNode;
    function FirstChild: TJvXMLNode;
    function LastChild: TJvXMLNode;
    function PreviousSibling: TJvXMLNode;
    function NextSibling: TJvXMLNode;
    function MoveAddNode(Dest: TJvXMLNode): TJvXMLNode;
    function MoveInsertNode(Dest: TJvXMLNode): TJvXMLNode;
    function RemoveChildNode(ANode: TJvXMLNode): TJvXMLNode;
    property Name: string read FName write FName;
    property Value: Variant read FValue write FValue;
    property ValueType: TJvXMLValueType read FValueType write FValueType;
    property Nodes: TList read FNodes write FNodes;
    property ParentNode: TJvXMLNode read FParentNode write FParentNode;
    property Attributes: TList read FAttributes write FAttributes;
  end;

  TJvXMLTree = class(TJvXMLNode)
  private
    FLines: TStringList;
    FNodeCount: Integer;
    function GetLines: TStrings;
    procedure SetLines(const Value: TStrings);
    function GetText: string;
    procedure SetText(const Value: string);
  public
    constructor Create(const AName: string; AValue: Variant; AParent: TJvXMLNode);
    destructor Destroy; override;
    procedure ParseXML;
    procedure LoadFromFile(const FileName: string);
    procedure LoadFromStream(Stream: TStream);
    procedure SaveToFile(const FileName: string);
    procedure SaveToStream(Stream: TStream);
    function AsText: string;
    property Lines: TStrings read GetLines write SetLines;
    property NodeCount: Integer read FNodeCount;
    property Text: string read GetText write SetText;
  end;

procedure PreProcessXML(AList: TStrings);

{$IFDEF UNITVERSIONING}
const
  UnitVersioning: TUnitVersionInfo = (
    RCSfile: '$URL$';
    Revision: '$Revision$';
    Date: '$Date$';
    LogPath: 'JVCL\run'
  );
{$ENDIF UNITVERSIONING}

implementation

uses
  JvConsts;

procedure PreProcessXML(AList: TStrings);
var
  OList: TStringList;
  S, xTag, xText, xData: string;
  P1, P2, C: Integer;
  //Level: Integer;

  function Clean(const AText: string): string;
  begin
    Result := StringReplace(AText, sLineBreak, ' ', [rfReplaceAll]);
    Result := StringReplace(Result, Tab, ' ', [rfReplaceAll]);
    Result := Trim(Result);
  end;

  function CleanCDATA(const AText: string): string;
  begin
    Result := StringReplace(AText, sLineBreak, '\n ', [rfReplaceAll]);
    Result := StringReplace(Result, Tab, '\t ', [rfReplaceAll]);
  end;

begin
  OList := TStringList.Create;
  try
    S := AList.Text;
    xText := '';
    xTag := '';
    P1 := 1;
    C := Length(S);
    //Level := 0;
    repeat
      P2 := PosStr('<', S, P1);
      if P2 > 0 then
      begin
        xText := Trim(Copy(S, P1, P2 - P1));
        if xText <> '' then
          OList.Add('TX:' + Clean(xText));
        P1 := P2;
        // check for CDATA
        if UpperCase(Copy(S, P1, 9)) = '<![CDATA[' then
        begin
          P2 := PosStr(']]>', S, P1);
          xData := Copy(S, P1 + 9, P2 - P1 - 9);
          OList.Add('CD:' + CleanCDATA(xData));
          P1 := P2 + 2;
        end
        else
        begin
          P2 := PosStr('>', S, P1);
          if P2 > 0 then
          begin
            xTag := Copy(S, P1 + 1, P2 - P1 - 1);
            P1 := P2;
            if xTag[1] = '/' then
            begin
              Delete(xTag, 1, 1);
              OList.Add('CT:' + Clean(xTag));
              //Dec(Level);
            end
            else
            if xTag[Length(xTag)] = '/' then
              OList.Add('ET:' + Clean(xTag))
            else
            begin
              //Inc(Level);
              OList.Add('OT:' + Clean(xTag));
            end;
          end;
        end;
      end
      else
      begin
        xText := Trim(Copy(S, P1, Length(S)));
        if xText <> '' then
        begin
          OList.Add('TX:' + Clean(xText));
        end;
        P1 := C;
      end;
      Inc(P1);
    until P1 > C;
    AList.Assign(OList);
  finally
    OList.Free;
  end;
end;

//=== { TJvXMLNode } =========================================================

constructor TJvXMLNode.Create(const AName: string; AValue: Variant; AParent: TJvXMLNode);
begin
  inherited Create;
  FNodes := TList.Create;
  FName := AName;
  FValue := AValue;
  FValueType := xvtString;
  FParentNode := AParent;
  FAttributes := TList.Create;
end;

destructor TJvXMLNode.Destroy;
begin
  ClearNodes;
  FNodes.Free;
  ClearAttributes;
  FAttributes.Free;
  inherited Destroy;
end;

function TJvXMLNode.AddAttribute(const AName: string; AValue: Variant): TJvXMLAttribute;
begin
  Result := TJvXMLAttribute.Create(Self, AName, AValue);
  Attributes.Add(Result);
end;

function TJvXMLNode.AddNode(const AName: string; AValue: Variant): TJvXMLNode;
begin
  Result := TJvXMLNode.Create(AName, AValue, Self);
  Nodes.Add(Result);
end;

// adds node and parses any attributes;

function TJvXMLNode.AddNodeEx(const AName: string; AValue: Variant): TJvXMLNode;
var
  S, SN, SV: string;
  C, P1, P2: Integer;
begin
  Result := TJvXMLNode.Create(AName, AValue, Self);
  Self.Nodes.Add(Result);
  C := Length(AName);
  //first parse Name
  P1 := PosStr(' ', AName, 1);
  if P1 = 0 then
    Exit;
  S := Copy(AName, 1, P1 - 1);
  Result.Name := S;
  repeat
    // find '='
    P2 := PosStr('=', AName, P1);
    if P2 = 0 then
      Break;
    SN := Trim(Copy(AName, P1, P2 - P1));
    P1 := P2;
    // find begin of value
    P1 := PosStr('"', AName, P1);
    if P1 = 0 then
      Break;
    P2 := PosStr('"', AName, P1 + 1);
    if P2 = 0 then
      Exit;
    SV := Copy(AName, P1 + 1, P2 - P1 - 1);
    Result.AddAttribute(SN, SV);
    P1 := P2 + 1;
  until P1 > C;
end;

function TJvXMLNode.GetNamedAttribute(const AName: string): TJvXMLAttribute;
var
  I: Integer;
  N: TJvXMLAttribute;
begin
  Result := nil;
  for I := 0 to Attributes.Count - 1 do
  begin
    N := TJvXMLAttribute(Attributes[I]);
    if N.Name = AName then
    begin
      Result := N;
      Break;
    end;
  end;
end;

procedure TJvXMLNode.ClearAttributes;
var
  I: Integer;
begin
  if Attributes.Count <> 0 then
  begin
    for I := 0 to Attributes.Count - 1 do
      TJvXMLAttribute(Attributes[I]).Free;
    Attributes.Clear;
  end;
end;

procedure TJvXMLNode.ClearNodes;
var
  I: Integer;
begin
  for I := 0 to Nodes.Count - 1 do
    TJvXMLNode(Nodes[I]).Free;
  Nodes.Clear;
end;

procedure TJvXMLNode.DeleteAttribute(Index: Integer);
begin
  if (Attributes.Count > 0) and (Index < Attributes.Count) then
  begin
    TJvXMLAttribute(Attributes[Index]).Free;
    Attributes.Delete(Index);
  end;
end;

procedure TJvXMLNode.DeleteNode(Index: Integer);
begin
  if (Nodes.Count > 0) and (Index < Nodes.Count) then
  begin
    TJvXMLNode(Nodes[Index]).Free;
    Nodes.Delete(Index);
  end;
end;

function TJvXMLNode.Document(ALevel: Integer): string;
var
  I: Integer;
  Indent: string;

  function ExpandCDATA(const AValue: string): string;
  begin
    Result := StringReplace(AValue, '\n ', sLineBreak, [rfReplaceAll]);
    Result := StringReplace(Result, '\t ', Tab, [rfReplaceAll]);
  end;

begin
  if ALevel > 0 then
    Indent := StringOfChar(' ', ALevel * 2)
  else
    Indent := '';
  Result := Indent + '<' + Name;
  if Attributes.Count > 0 then
    for I := 0 to Attributes.Count - 1 do
      Result := Result + TJvXMLAttribute(Attributes[I]).Document;
  if (Nodes.Count = 0) and (Value = '') then
  begin
    Result := Result + ' />' + sLineBreak;
    Exit;
  end
  else
    Result := Result + '>' + sLineBreak;
  if Value <> '' then
  begin
    if ValueType = xvtString then
      Result := Result + Indent + '  ' + Value + sLineBreak
    else
    if ValueType = xvtCDATA then
      Result := Result + Indent + '  ' + '<![CDATA[' + ExpandCDATA(Value) + ']]>' + sLineBreak;
  end;
  if Nodes.Count <> 0 then
    for I := 0 to Nodes.Count - 1 do
      Result := Result + TJvXMLNode(Nodes[I]).Document(ALevel + 1);
  Result := Result + Indent + '</' + Name + '>' + sLineBreak;
end;

// duplicates a node recursively

function TJvXMLNode.CloneNode: TJvXMLNode;
var
  I: Integer;
  N: TJvXMLNode;
begin
  Result := TJvXMLNode.Create(Name, Value, nil);
  Result.Name := Name;
  Result.Value := Value;
  if Attributes.Count > 0 then
    for I := 0 to Attributes.Count - 1 do
      Result.AddAttribute(TJvXMLAttribute(Attributes[I]).Name, TJvXMLAttribute(Attributes[I]).Value);
  if Nodes.Count > 0 then
    for I := 0 to Nodes.Count - 1 do
    begin
      N := TJvXMLNode(Nodes[I]).CloneNode;
      Result.Nodes.Add(N);
    end;
end;

function TJvXMLNode.GetNamedNode(const AName: string): TJvXMLNode;
var
  I: Integer;
  N: TJvXMLNode;
begin
  Result := nil;
  for I := 0 to Nodes.Count - 1 do
  begin
    N := TJvXMLNode(Nodes[I]);
    if N.Name = AName then
    begin
      Result := N;
      Exit;
    end;
  end;
end;

function TJvXMLNode.FirstChild: TJvXMLNode;
begin
  if Nodes.Count > 0 then
    Result := TJvXMLNode(Nodes[0])
  else
    Result := nil;
end;

function TJvXMLNode.LastChild: TJvXMLNode;
begin
  if Nodes.Count > 0 then
    Result := TJvXMLNode(Nodes[Nodes.Count - 1])
  else
    Result := nil;
end;

function TJvXMLNode.NextSibling: TJvXMLNode;
var
  Index: Integer;
begin
  Result := nil;
  if ParentNode = nil then
    Exit;
  Index := ParentNode.Nodes.IndexOf(Self);
  if Index = -1 then
    Exit;
  if Index < ParentNode.Nodes.Count - 1 then
    Result := TJvXMLNode(ParentNode.Nodes[Index + 1]);
end;

function TJvXMLNode.PreviousSibling: TJvXMLNode;
var
  Index: Integer;
begin
  Result := nil;
  if ParentNode = nil then
    Exit;
  Index := ParentNode.Nodes.IndexOf(Self);
  if Index = -1 then
    Exit;
  if Index > 0 then
    Result := TJvXMLNode(ParentNode.Nodes[Index - 1]);
end;
// moves a node to a new location

function TJvXMLNode.MoveInsertNode(Dest: TJvXMLNode): TJvXMLNode;
var
  Index1, Index2: Integer;
begin
  Result := nil;
  if Dest.ParentNode = nil then
    Exit; // can not move to root
  Index1 := Self.ParentNode.Nodes.IndexOf(Self);
  if Index1 = -1 then
    Exit;
  Index2 := Dest.ParentNode.Nodes.IndexOf(Dest);
  if Index2 = -1 then
    Exit;
  Dest.ParentNode.Nodes.Insert(Index2, Self);
  Self.ParentNode.Nodes.Delete(Index1);
  Self.ParentNode := Dest.ParentNode;
  Result := Self;
end;

function TJvXMLNode.MoveAddNode(Dest: TJvXMLNode): TJvXMLNode;
var
  Index: Integer;
begin
  Result := nil;
  if Dest = nil then
    Exit; // can not move to root
  Index := Self.ParentNode.Nodes.IndexOf(Self);
  if Index = -1 then
    Exit;
  Dest.Nodes.Add(Self);
  Self.ParentNode.Nodes.Delete(Index);
  Self.ParentNode := Dest;
  Result := Self;
end;

// removes and Frees the childnode recursively.
// returns Self when done, or nil in case of error

function TJvXMLNode.RemoveChildNode(ANode: TJvXMLNode): TJvXMLNode;
var
  Index: Integer;
begin
  Result := nil;
  Index := Nodes.IndexOf(ANode);
  if Index = -1 then
    Exit;
  Nodes.Delete(Index);
  ANode.Free;
  Result := Self;
end;

function TJvXMLNode.HasChildNodes: Boolean;
begin
  Result := Nodes.Count > 0;
end;

procedure TJvXMLNode.GetAttributeNames(AList: TStrings);
var
  I: Integer;
begin
  AList.Clear;
  for I := 0 to Attributes.Count - 1 do
    AList.Add(TJvXMLAttribute(Attributes[I]).Name);
end;

procedure TJvXMLNode.GetNodeNames(AList: TStrings);
var
  I, C: Integer;
begin
  AList.Clear;
  C := Nodes.Count;
  for I := 0 to C - 1 do
    AList.Add(TJvXMLNode(Nodes[I]).Name);
end;

function TJvXMLNode.GetNodePath: string;
var
  N: TJvXMLNode;
begin
  N := Self;
  Result := Name;
  while N.ParentNode <> nil do
  begin
    N := N.ParentNode;
    Result := N.Name + '/' + Result;
  end;
end;

// search recursively for a named node

function TJvXMLNode.FindNamedNode(const AName: string): TJvXMLNode;
var
  I: Integer;
  N: TJvXMLNode;
begin
  Result := nil;
  for I := 0 to Nodes.Count - 1 do
  begin
    N := TJvXMLNode(Nodes[I]);
    if N.Name = AName then
    begin
      Result := N;
      Break;
    end
    else
    begin // Recurse
      Result := N.FindNamedNode(AName);
      if Result <> nil then
        Break;
    end;
  end;
end;

// add all found named Nodes to AList

procedure TJvXMLNode.FindNamedNodes(const AName: string; AList: TList);
var
  I: Integer;
  N: TJvXMLNode;
begin
  for I := 0 to Nodes.Count - 1 do
  begin
    N := TJvXMLNode(Nodes[I]);
    if N.Name = AName then
      AList.Add(N);
    // Recurse
    N.FindNamedNodes(AName, AList);
  end;
end;

// add recursively all Nodes to AList
// the list only contains pointers to the Nodes
// typecast to use, e.g. N:=TJvXMLNode(AList[0]);

procedure TJvXMLNode.GetAllNodes(AList: TList);
var
  I: Integer;
  N: TJvXMLNode;
begin
  for I := 0 to Nodes.Count - 1 do
  begin
    N := TJvXMLNode(Nodes[I]);
    AList.Add(N);
    // Recurse
    N.GetAllNodes(AList);
  end;
end;

// add recursively all Nodes with matching named attribute to AList
// the list only contains pointers to the Nodes
// typecast to use, e.g. N:=TJvXMLNode(AList[0]);

procedure TJvXMLNode.FindNamedAttributes(const AName: string; AList: TList);
var
  I: Integer;
begin
  for I := 0 to Attributes.Count - 1 do
    if TJvXMLAttribute(Attributes[I]).Name = AName then
    begin
      AList.Add(Self);
      Break;
    end;
  for I := 0 to Nodes.Count - 1 do
    TJvXMLNode(Nodes[I]).FindNamedAttributes(AName, AList);
end;

{
this procedure adds the node to AList when it matches the pattern
this will be the key procedure for XSL implementation
only basic matching is provided in the first release
path operators
 /  child path
 // recursive descent
 .  curren context or node
 @  attribute
 *  wildcar
some examples
 /  the root node only
 book/author  <author> elements that are children of <book> elements
 // the root node and all Nodes below
 //*  all element Nodes below the root node
 book//author  <author> elements that are descendants of <book> elements
 .//author  <author elements that are descendants of the current element
 *  non-root elements, irrespective of the element Name
 book/*  elements that are children of <book> elements
 book//* elements that are descendants of <book> elements
 book/*/author  <author> elements that are grandchildren of <book> elements
 book/@print_date print_date attributes that are attached to <book> elements
 */@print_date print_date atrtributes that are attached to any elements

Index can be used to specify a particular node within a matching set
 /booklist/book[0]  First <book> node in root <booklist> element
 /booklist/book[2]  Third <book> node in root <booklist> element
 /booklist/book[end()] Last <book> node in root <booklist> element
}

procedure TJvXMLNode.MatchPattern(const APattern: string; AList: TList);
begin
  // to be implemented
end;

{select a node based on path info
 e.g. booklist/book/category will find the first
 <category> that is a child of <book> that is a child of <booklist>
 }

function TJvXMLNode.SelectSingleNode(const APattern: string): TJvXMLNode;
var
  NPattern, LFilter: string;
  P, I: Integer;
  N: TJvXMLNode;
  ObjFilter: TJvXMLFilter;
begin
  Result := nil;
  if Nodes.Count = 0 then
    Exit;
  ObjFilter := nil;
  try
    P := Pos('/', APattern);
    if P = 0 then
    begin
      ObjFilter := TJvXMLFilter.Create(APattern);
      for I := 0 to Nodes.Count - 1 do
      begin
        N := TJvXMLNode(Nodes[I]);
        if N.MatchFilter(ObjFilter) then
        begin
          Result := N;
          Exit;
        end;
      end;
      // not found;
    end
    else
    begin
      LFilter := Copy(APattern, 1, P - 1);
      NPattern := Copy(APattern, P + 1, Length(APattern));
      ObjFilter := TJvXMLFilter.Create(LFilter);
      for I := 0 to Nodes.Count - 1 do
      begin
        N := TJvXMLNode(Nodes[I]);
        if N.MatchFilter(ObjFilter) then
        begin
          Result := N.SelectSingleNode(NPattern);
          if Result <> nil then
            Exit;
        end;
      end;
    end;
  finally
    ObjFilter.Free;
  end;
end;

// filter contains Name + any filters between []

function TJvXMLNode.MatchFilter(AObjFilter: TJvXMLFilter): Boolean;
var
  I, J: Integer;
  A: TJvXMLAttribute;
  N: TJvXMLNode;
  AttName: string;
  Atom: TJvXMLFilterAtom;
  AttResult: Boolean;

  function EvalAtom(const AValue: string): Boolean;
  begin
    Result := False;
    case Atom.Operator of
      xfoNOP:
        Result := True;
      xfoEQ:
        Result := AValue = Atom.Value;
      xfoIEQ:
        Result := AnsiCompareText(AValue, Atom.Value) = 0;
      xfoNE:
        Result := AValue <> Atom.Value;
      xfoINE:
        Result := AnsiCompareText(AValue, Atom.Value) <> 0;
      xfoGT:
        try
          Result := StrToFloat(AValue) > StrToFloat(Atom.Value);
        except
        end;
      xfoIGT:
        Result := AnsiCompareText(AValue, Atom.Value) > 0;
      xfoLT:
        try
          Result := StrToFloat(AValue) < StrToFloat(Atom.Value);
        except
        end;
      xfoILT:
        Result := AnsiCompareText(AValue, Atom.Value) < 0;
      xfoGE:
        try
          Result := StrToFloat(AValue) >= StrToFloat(Atom.Value);
        except
        end;
      xfoIGE:
        Result := AnsiCompareText(AValue, Atom.Value) >= 0;
      xfoLE:
        try
          Result := StrToFloat(AValue) <= StrToFloat(Atom.Value);
        except
        end;
      xfoILE:
        Result := AnsiCompareText(AValue, Atom.Value) <= 0;
    end;
  end;

begin
  Result := False;
  AttResult := False;
  if AObjFilter.Filters.Count = 0 then
  begin // just filter on Name
    Result := AObjFilter.Name = Name;
    Exit;
  end;
  for I := 0 to AObjFilter.Filters.Count - 1 do
  begin
    Atom := TJvXMLFilterAtom(AObjFilter.Filters[I]);
    if Atom.AttributeFilter then
    begin
      AttName := Atom.Name;
      if AttName = '*' then
      begin // match any attribute
        if Attributes.Count = 0 then
          Exit;
        for J := 0 to Attributes.Count - 1 do
        begin
          A := TJvXMLAttribute(Attributes[J]);
          AttResult := EvalAtom(A.Value);
          if AttResult then
            Break;
        end;
        if not AttResult then
          Exit;
      end
      else
      begin
        A := GetNamedAttribute(AttName);
        if (A = nil) or not EvalAtom(A.Value) then
          Exit;
      end;
    end
    else
    begin
      AttName := Atom.Name;
      N := GetNamedNode(AttName);
      if (N = nil) or not EvalAtom(N.Value) then
        Exit;
    end;
  end;
  Result := True;
end;

procedure TJvXMLNode.SelectNodes(APattern: string; AList: TList);
var
  NPattern: string;
  P, I: Integer;
  N: TJvXMLNode;
  LFilter: string;
  ObjFilter: TJvXMLFilter;
  Recurse: Boolean;
begin
  if Nodes.Count = 0 then
    Exit;
  if Copy(APattern, 1, 2) = '//' then
  begin //recursive
    Delete(APattern, 1, 2);
    Recurse := True;
  end
  else
    Recurse := False;
  P := Pos('/', APattern);
  if P = 0 then
  begin
    LFilter := APattern;
    ObjFilter := TJvXMLFilter.Create(LFilter);
    for I := 0 to Nodes.Count - 1 do
    begin
      N := TJvXMLNode(Nodes[I]);
      if N.MatchFilter(ObjFilter) then
        AList.Add(N)
      else
      if Recurse then
        N.SelectNodes('//' + APattern, AList);
    end;
    ObjFilter.Free;
  end
  else
  begin
    LFilter := Copy(APattern, 1, P - 1);
    if Copy(APattern, P, 2) = '//' then
      NPattern := Copy(APattern, P, Length(APattern))
    else
      NPattern := Copy(APattern, P + 1, Length(APattern));
    ObjFilter := TJvXMLFilter.Create(LFilter);
    for I := 0 to Nodes.Count - 1 do
    begin
      N := TJvXMLNode(Nodes[I]);
      if N.MatchFilter(ObjFilter) then
        N.SelectNodes(NPattern, AList)
      else
      if Recurse then
        N.SelectNodes('//' + APattern, AList);
    end;
    ObjFilter.Free;
  end;
end;

// the XSL implementation
// although this function returns a string, the string itself can be parsed to Create a DOM

function TJvXMLNode.TransformNode(AStyleSheet: TJvXMLNode): string;
begin
  // to be implemented;
  Result := AStyleSheet.Process(0, Self);
end;

// used in conjunction with the TransformNode function.
// basically works like the Document function except for Nodes with processing instructions

function TJvXMLNode.Process(ALevel: Integer; ANode: TJvXMLNode): string;
var
  I: Integer;
  Indent: string;

  function ExpandCDATA(const AValue: string): string;
  begin
    Result := StringReplace(AValue, '\n ', sLineBreak, [rfReplaceAll]);
    Result := StringReplace(Result, '\t ', Tab, [rfReplaceAll]);
  end;

begin
  if ParentNode = nil then
  begin
    for I := 0 to Nodes.Count - 1 do
      Result := Result + TJvXMLNode(Nodes[I]).Process(ALevel + 1, ANode);
    Exit;
  end;
  if ALevel > 0 then
    Indent := StringOfChar(' ', ALevel * 2)
  else
    Indent := '';
  Result := Indent + '<' + Name;
  for I := 0 to Attributes.Count - 1 do
    Result := Result + TJvXMLAttribute(Attributes[I]).Document;
  if (Nodes.Count = 0) and (Value = '') then
  begin
    Result := Result + ' />' + sLineBreak;
    Exit;
  end
  else
    Result := Result + '>' + sLineBreak;
  if Value <> '' then
  begin
    if ValueType = xvtString then
      Result := Result + Indent + '  ' + Value + sLineBreak
    else
    if ValueType = xvtCDATA then
      Result := Result + Indent + '  ' + '<![CDATA[' + ExpandCDATA(Value) + ']]>' + sLineBreak;
  end;
  for I := 0 to Nodes.Count - 1 do
    Result := Result + TJvXMLNode(Nodes[I]).Process(ALevel + 1, ANode);
  Result := Result + Indent + '</' + Name + '>' + sLineBreak;
end;

function TJvXMLNode.GetNameSpace: string;
var
  P: Integer;
begin
  P := Pos(':', FName);
  if P > 0 then
    Result := Copy(FName, 1, P - 1)
  else
    Result := '';
end;

// find the node with a path like customers/regional/jansoft

function TJvXMLNode.GetNamePathNode(const APath: string): TJvXMLNode;
var
  AName, NewPath, SIndex: string;
  I, P, Index, IndexC: Integer;
  N: TJvXMLNode;
begin
  Result := nil;
  if Nodes.Count = 0 then
    Exit;
  if APath = '' then
  begin
    Result := Self;
    Exit;
  end;
  P := PosStr('/', APath, 1);
  if P = 0 then
  begin
    AName := APath;
    NewPath := '';
  end
  else
  begin
    AName := Copy(APath, 1, P - 1);
    NewPath := Copy(APath, P + 1, Length(APath));
  end;
  // now check for any Index []
  P := PosStr('[', AName, 1);
  Index := 0; // search first by default
  IndexC := 0;
  if P > 0 then
  begin
    SIndex := Copy(AName, P + 1, Length(AName) - P - 1);
    AName := Copy(AName, 1, P - 1);
    if SIndex = 'end' then
      Index := -1
    else
      try
        Index := StrToInt(SIndex);
        if Index >= Nodes.Count then
          Exit;
      except
        Exit;
      end;
  end;
  if Index = -1 then // search end from end
    for I := Nodes.Count - 1 downto 0 do
    begin
      N := TJvXMLNode(Nodes[I]);
      if N.Name = AName then
        if NewPath = '' then
        begin
          Result := N;
          Exit;
        end
        else
        begin
          Result := N.GetNamePathNode(NewPath);
          Exit;
        end;
    end
  else // search from beginning indexed
    for I := 0 to Nodes.Count - 1 do
    begin
      N := TJvXMLNode(Nodes[I]);
      if N.Name = AName then
        if Index = IndexC then
        begin
          if NewPath = '' then
          begin
            Result := N;
            Exit;
          end
          else
          begin
            Result := N.GetNamePathNode(NewPath);
            Exit;
          end;
        end
        else
          Inc(IndexC);
    end;
end;

function TJvXMLNode.ForceNamePathNode(const APath: string): TJvXMLNode;
var
  AName, NewPath: string;
  I, P: Integer;
  N: TJvXMLNode;
  DoAppend: Boolean;
begin
  //  Result:=nil;
  P := PosStr('/', APath, 1);
  if P = 0 then
  begin
    AName := APath;
    NewPath := '';
  end
  else
  begin
    AName := Copy(APath, 1, P - 1);
    NewPath := Copy(APath, P + 1, Length(APath));
  end;
  P := PosStr('+', AName, 1);
  if P > 0 then
    Delete(AName, P, 1);
  DoAppend := P > 0;
  if not DoAppend then
    for I := 0 to Nodes.Count - 1 do
    begin
      N := TJvXMLNode(Nodes[I]);
      if N.Name = AName then
        if NewPath = '' then
        begin
          Result := N;
          Exit;
        end
        else
        begin
          Result := N.ForceNamePathNode(NewPath);
          Exit;
        end;
    end;
  // we dont have it , so force it;
  N := TJvXMLNode.Create(AName, '', Self);
  Nodes.Add(N);
  if NewPath = '' then
    Result := N
  else
    Result := N.ForceNamePathNode(NewPath);
end;

function TJvXMLNode.ForceNamePathNodeAttribute(const APath, AName: string;
  AValue: Variant): TJvXMLAttribute;
var
  N: TJvXMLNode;
  A: TJvXMLAttribute;
begin
  Result := nil;
  N := ForceNamePathNode(APath);
  if N = nil then
    Exit;
  A := N.GetNamedAttribute(AName);
  if A <> nil then
  begin
    A.Value := AValue;
    Result := A;
  end
  else
    Result := N.AddAttribute(AName, AValue);
end;

function TJvXMLNode.GetNamePathNodeAttribute(const APath, AName: string): TJvXMLAttribute;
var
  N: TJvXMLNode;
begin
  Result := nil;
  N := GetNamePathNode(APath);
  if N = nil then
    Exit;
  Result := N.GetNamedAttribute(AName);
end;

procedure TJvXMLNode.DeleteNamePathNode(const APath: string);
var
  N, PN: TJvXMLNode;
  I: Integer;
begin
  if APath = '' then
    Exit;
  N := GetNamePathNode(APath);
  if N = nil then
    Exit;
  PN := N.ParentNode;
  for I := 0 to PN.Nodes.Count - 1 do
    if TJvXMLNode(PN.Nodes[I]) = N then
    begin
      PN.DeleteNode(I);
      Exit;
    end;
end;

procedure TJvXMLNode.DeleteNamePathNodeAttribute(const APath, AName: string);
var
  A: TJvXMLAttribute;
  PN: TJvXMLNode;
  I: Integer;
begin
  A := GetNamePathNodeAttribute(APath, AName);
  if A = nil then
    Exit;
  PN := A.Parent;
  for I := 0 to PN.Attributes.Count - 1 do
    if TJvXMLAttribute(PN.Attributes[I]) = A then
    begin
      PN.DeleteAttribute(I);
      Exit;
    end;
end;

function TJvXMLNode.GetAttributeValue(const AName: string): Variant;
var
  I: Integer;
  A: TJvXMLAttribute;
begin
  Result := Null;
  for I := 0 to Attributes.Count - 1 do
  begin
    A := TJvXMLAttribute(Attributes[I]);
    if A.Name = AName then
    begin
      Result := A.Value;
      Exit;
    end;
  end;
end;

//=== { TJvXMLTree } =========================================================

constructor TJvXMLTree.Create(const AName: string; AValue: Variant; AParent: TJvXMLNode);
begin
  inherited Create(AName, AValue, AParent);
  FLines := TStringList.Create;
end;

destructor TJvXMLTree.Destroy;
begin
  FLines.Free;
  inherited Destroy;
end;

function TJvXMLTree.AsText: string;
var
  I: Integer;
begin
  if Nodes.Count = 0 then
    Exit;
  Result := '<' + Name;
  if Attributes.Count > 0 then
    for I := 0 to Attributes.Count - 1 do
      Result := Result + TJvXMLAttribute(Attributes[I]).Document;
  Result := Result + '>' + sLineBreak;
  for I := 0 to Nodes.Count - 1 do
    Result := Result + TJvXMLNode(Nodes[I]).Document(1);
  Result := Result + '</' + Name + '>' + sLineBreak;
end;

procedure TJvXMLTree.SaveToFile(const FileName: string);
begin
  Lines.Text := Text;
  Lines.SaveToFile(FileName);
end;

function TJvXMLTree.GetLines: TStrings;
begin
  Result := FLines;
end;

procedure TJvXMLTree.SetLines(const Value: TStrings);
begin
  FLines.Assign(Value);
end;

procedure TJvXMLTree.LoadFromStream(Stream: TStream);
begin
  ClearNodes;
  ClearAttributes;
  Lines.LoadFromStream(Stream);
  PreProcessXML(Lines);
  ParseXML;
end;

procedure TJvXMLTree.SaveToStream(Stream: TStream);
begin
  Lines.Text := AsText;
  Lines.SaveToStream(Stream);
end;

function TJvXMLTree.GetText: string;
var
  I: Integer;
begin
  //  Result:='<'+Name;
  //  if Attributes.Count>0 then
  //  for I:=0 to Attributes.Count-1 do
  //    Result:=Result+TJvXMLAttribute(Attributes[I]).Document;
  //  Result:=Result+'>'+sLineBreak;
  Result := '';
  for I := 0 to Nodes.Count - 1 do
    Result := Result + TJvXMLNode(Nodes[I]).Document(0);
  //  Result:=Result+'</'+Name+'>'+sLineBreak;
end;

procedure TJvXMLTree.SetText(const Value: string);
begin
  ClearNodes;
  ClearAttributes;
  Lines.Text := Value;
  PreProcessXML(Lines);
  ParseXML;
end;

//=== { TJvXMLAttribute } ====================================================

constructor TJvXMLAttribute.Create(AParent: TJvXMLNode; const AName: string; AValue: Variant);
begin
  inherited Create;
  FName := AName;
  FValue := AValue;
  FParent := AParent;
end;

function TJvXMLAttribute.Document: string;
var
  S: string;
begin
  S := Value;
  Result := ' ' + Name + '="' + S + '"';
end;

//=== { TJvXMLTree } =========================================================

procedure TJvXMLTree.ParseXML;
var
  I, C: Integer;
  S, Token, AName: string;
  N: TJvXMLNode;
begin
  FNodeCount := 0;
  ClearNodes;
  ClearAttributes;
  Name := 'root';
  N := Self;
  C := Lines.Count - 1;
  I := 0;
  while I <= C do
  begin
    S := Lines[I];
    Token := Copy(S, 1, 3);
    AName := Copy(S, 4, Length(S));
    if Token = 'OT:' then
    begin
      N := N.AddNodeEx(AName, '');
      Inc(FNodeCount);
    end
    else
    if Token = 'CT:' then
      N := N.ParentNode
    else
    if Token = 'ET:' then
      N.AddNodeEx(AName, '')
    else
    if Token = 'TX:' then
    begin
      N.Value := AName;
      N.ValueType := xvtString;
    end
    else
    if Token = 'CD:' then
    begin
      N.Value := AName;
      N.ValueType := xvtCDATA;
    end;
    Inc(I);
  end;
end;

procedure TJvXMLTree.LoadFromFile(const FileName: string);
begin
  ClearNodes;
  ClearAttributes;
  Lines.LoadFromFile(FileName);
  PreProcessXML(Lines);
  ParseXML;
end;

//=== { TJvXMLFilter } =======================================================

constructor TJvXMLFilter.Create(const FilterStr: string);
begin
  inherited Create;
  Filters := TList.Create;
  Initialize(FilterStr);
end;

destructor TJvXMLFilter.Destroy;
var
  I: Integer;
begin
  for I := 0 to Filters.Count - 1 do
    TJvXMLFilterAtom(Filters[I]).Free;
  Filters.Free;
  inherited Destroy;
end;

procedure TJvXMLFilter.Initialize(FilterStr: string);
var
  LFilter: string;
  P1, P2: Integer;
  AttName, AttValue: string;
  AttOperator: TJvXMLFilterOperator;
  Atom: TJvXMLFilterAtom;
  //    A: TJvXMLAttribute;
  //    N: TJvXMLNode;

  function TrimQuotes(const S: string): string;
  var
    L: Integer;
  begin
    Result := Trim(S);
    if S = '' then
      Exit;
    if (S[1] = '"') or (S[1] = '''') then
      Delete(Result, 1, 1);
    if S = '' then
      Exit;
    L := Length(Result);
    if (Result[L] = '"') or (Result[L] = '''') then
      Delete(Result, L, 1);
  end;

  function SplitNameValue(const S: string): Boolean;
  var
    PP: Integer;
  begin
    // (rom) inefficient implementation
    //      Result:=False;
    PP := PosStr(' $ne$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoNE;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 6, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $ine$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoINE;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 7, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $ge$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoGE;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 6, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $ige$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoIGE;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 7, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $gt$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoGT;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 6, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $igt$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoIGT;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 7, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $le$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoLE;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 6, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $ile$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoILE;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 7, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $lt$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoLT;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 6, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $ilt$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoILT;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 7, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $eq$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoEQ;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 6, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' $ieq$ ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoIEQ;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 7, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    PP := PosStr(' = ', S, 1);
    if PP > 0 then
    begin
      AttOperator := xfoEQ;
      AttName := Trim(Copy(S, 1, PP - 1));
      AttValue := TrimQuotes(Copy(S, PP + 3, Length(S)));
      Result := (AttName <> '') and (AttValue <> '');
      Exit;
    end;
    AttOperator := xfoNOP;
    AttName := S;
    AttValue := '';
    Result := True;
    Exit;
  end;

begin
  P1 := PosStr('[', FilterStr, 1);
  if P1 = 0 then
  begin // just a Name filter on Name
    Name := FilterStr;
    Exit;
  end
  else
  begin
    Name := Copy(FilterStr, 1, P1 - 1);
    Delete(FilterStr, 1, P1 - 1);
  end;
  repeat
    FilterStr := Trim(FilterStr);
    P1 := PosStr('[', FilterStr, 1);
    if P1 = 0 then
      Exit;
    P2 := PosStr(']', FilterStr, P1 + 1);
    if P2 = 0 then
      Exit;
    LFilter := Copy(FilterStr, P1 + 1, P2 - P1 - 1);
    Delete(FilterStr, 1, P2);
    if LFilter = '' then
      Exit;
    // check for attribute filter
    if LFilter[1] = '@' then
    begin
      if not SplitNameValue(Copy(LFilter, 2, Length(LFilter))) then
        Exit;
      Atom := TJvXMLFilterAtom.Create;
      Atom.Name := AttName;
      Atom.Operator := AttOperator;
      Atom.Value := AttValue;
      Atom.AttributeFilter := True;
      Filters.Add(Atom);
    end
    else
    begin // childfilter
      if not SplitNameValue(LFilter) then
        Exit;
      Atom := TJvXMLFilterAtom.Create;
      Atom.Name := AttName;
      Atom.Operator := AttOperator;
      Atom.Value := AttValue;
      Atom.AttributeFilter := False;
      Filters.Add(Atom);
    end;
  until FilterStr = '';
end;

{$IFDEF UNITVERSIONING}
initialization
  RegisterUnitVersion(HInstance, UnitVersioning);

finalization
  UnregisterUnitVersion(HInstance);
{$ENDIF UNITVERSIONING}

end.
