+ XML writer now performs the namespace normalization.

git-svn-id: trunk@13789 -
This commit is contained in:
sergei 2009-10-01 19:29:13 +00:00
parent eb18aa8831
commit 78b41cd8f6
3 changed files with 260 additions and 4 deletions

View File

@ -260,6 +260,7 @@ type
function CloneNode(deep: Boolean; ACloneOwner: TDOMDocument): TDOMNode; overload; virtual;
function FindNode(const ANodeName: DOMString): TDOMNode; virtual;
function CompareName(const name: DOMString): Integer; virtual;
property Flags: TNodeFlags read FFlags;
end;
TDOMNodeClass = class of TDOMNode;

View File

@ -20,7 +20,7 @@ unit xmlutils;
interface
uses
SysUtils;
SysUtils, Classes;
function IsXmlName(const Value: WideString; Xml11: Boolean = False): Boolean; overload;
function IsXmlName(Value: PWideChar; Len: Integer; Xml11: Boolean = False): Boolean; overload;
@ -40,6 +40,7 @@ function WStrLIComp(S1, S2: PWideChar; Len: Integer): Integer;
type
{$ifndef fpc}
PtrInt = LongInt;
TFPList = TList;
{$endif}
PPHashItem = ^PHashItem;
@ -100,6 +101,36 @@ type
destructor Destroy; override;
end;
TBinding = class
public
uri: WideString;
next: TBinding;
prevPrefixBinding: TObject;
Prefix: PHashItem;
end;
TAttributeAction = (aaUnchanged, aaPrefix, aaBoth);
TNSSupport = class(TObject)
private
FNesting: Integer;
FPrefixSeqNo: Integer;
FFreeBindings: TBinding;
FBindings: TFPList;
FBindingStack: array of TBinding;
FPrefixes: THashTable;
FDefaultPrefix: THashItem;
function GetBinding(const nsURI: WideString; aPrefix: PHashItem): TBinding;
public
constructor Create;
destructor Destroy; override;
procedure DefineBinding(const Prefix, nsURI: WideString; out Binding: TBinding);
function CheckAttribute(const Prefix, nsURI: WideString;
out Binding: TBinding): TAttributeAction;
procedure StartElement;
procedure EndElement;
end;
{$i names.inc}
implementation
@ -625,6 +656,144 @@ begin
result := False;
end;
{ TNSSupport }
constructor TNSSupport.Create;
begin
inherited Create;
FPrefixes := THashTable.Create(16, False);
FBindings := TFPList.Create;
SetLength(FBindingStack, 16);
end;
destructor TNSSupport.Destroy;
var
I: Integer;
begin
for I := FBindings.Count-1 downto 0 do
TObject(FBindings.List^[I]).Free;
FBindings.Free;
FPrefixes.Free;
inherited Destroy;
end;
function TNSSupport.GetBinding(const nsURI: WideString; aPrefix: PHashItem): TBinding;
begin
{ try to reuse an existing binding }
result := FFreeBindings;
if Assigned(result) then
FFreeBindings := result.Next
else { no free bindings, create a new one }
begin
result := TBinding.Create;
FBindings.Add(result);
end;
{ link it into chain of bindings at the current element level }
result.Next := FBindingStack[FNesting];
FBindingStack[FNesting] := result;
{ bind }
result.uri := nsURI;
result.Prefix := aPrefix;
result.PrevPrefixBinding := aPrefix^.Data;
aPrefix^.Data := result; // ** null binding not used here **
end;
procedure TNSSupport.DefineBinding(const Prefix, nsURI: WideString;
out Binding: TBinding);
var
Pfx: PHashItem;
begin
Pfx := @FDefaultPrefix;
if (nsURI <> '') and (Prefix <> '') then
Pfx := FPrefixes.FindOrAdd(PWideChar(Prefix), Length(Prefix));
if (Pfx^.Data = nil) or (TBinding(Pfx^.Data).uri <> nsURI) then
Binding := GetBinding(nsURI, Pfx)
else
Binding := nil;
end;
function TNSSupport.CheckAttribute(const Prefix, nsURI: WideString;
out Binding: TBinding): TAttributeAction;
var
Pfx: PHashItem;
I: Integer;
b: TBinding;
buf: array[0..31] of WideChar;
p: PWideChar;
begin
Binding := nil;
Pfx := nil;
if Prefix <> '' then
Pfx := FPrefixes.FindOrAdd(PWideChar(Prefix), Length(Prefix));
Result := aaUnchanged;
// no prefix, not bound, or bound to wrong URI
if (Pfx = nil) or (Pfx^.Data = nil) or (TBinding(Pfx^.Data).uri <> nsURI) then
begin
// see if there's another prefix bound to the target URI
// TODO: should use something faster than linear search
for i := FNesting downto 0 do
begin
b := FBindingStack[i];
while Assigned(b) do
begin
if (b.uri = nsURI) and (b.Prefix <> @FDefaultPrefix) then
begin
Binding := b; // found one -> override the attribute's prefix
Result := aaPrefix;
Exit;
end;
b := b.Next;
end;
end;
// no prefix, or bound (to wrong URI) -> must use generated prefix instead
if (Pfx = nil) or Assigned(Pfx^.Data) then
repeat
Inc(FPrefixSeqNo);
i := FPrefixSeqNo; // This is just 'NS'+IntToStr(FPrefixSeqNo);
p := @Buf[high(Buf)]; // done without using strings
while i <> 0 do
begin
p^ := WideChar(i mod 10+ord('0'));
dec(p);
i := i div 10;
end;
p^ := 'S'; dec(p);
p^ := 'N';
Pfx := FPrefixes.FindOrAdd(p, @Buf[high(Buf)]-p+1);
until Pfx^.Data = nil;
Binding := GetBinding(nsURI, Pfx);
Result := aaBoth;
end;
end;
procedure TNSSupport.StartElement;
begin
Inc(FNesting);
if FNesting >= Length(FBindingStack) then
SetLength(FBindingStack, FNesting * 2);
end;
procedure TNSSupport.EndElement;
var
b, temp: TBinding;
begin
temp := FBindingStack[FNesting];
while Assigned(temp) do
begin
b := temp;
temp := b.next;
b.next := FFreeBindings;
FFreeBindings := b;
b.Prefix^.Data := b.prevPrefixBinding;
end;
FBindingStack[FNesting] := nil;
if FNesting > 0 then
Dec(FNesting);
end;
initialization
finalization

View File

@ -37,7 +37,7 @@ procedure WriteXML(Element: TDOMNode; AStream: TStream); overload;
implementation
uses SysUtils;
uses SysUtils, xmlutils;
type
TSpecialCharCallback = procedure(c: WideChar) of object;
@ -51,6 +51,8 @@ type
FBufPos: PChar;
FCapacity: Integer;
FLineBreak: string;
FNSHelper: TNSSupport;
FScratch: TFPList;
procedure wrtChars(Src: PWideChar; Length: Integer);
procedure IncIndent;
procedure DecIndent; {$IFDEF HAS_INLINE} inline; {$ENDIF}
@ -62,6 +64,8 @@ type
const SpecialCharCallback: TSpecialCharCallback);
procedure AttrSpecialCharCallback(c: WideChar);
procedure TextNodeSpecialCharCallback(c: WideChar);
procedure WriteNSDef(B: TBinding);
procedure NamespaceFixup(Element: TDOMElement);
protected
procedure Write(const Buffer; Count: Longint); virtual; abstract;
procedure WriteNode(Node: TDOMNode);
@ -159,10 +163,14 @@ begin
// Later on, this may be put under user control
// for now, take OS setting
FLineBreak := sLineBreak;
FNSHelper := TNSSupport.Create;
FScratch := TFPList.Create;
end;
destructor TXMLWriter.Destroy;
begin
FScratch.Free;
FNSHelper.Free;
if FBufPos > FBuffer then
write(FBuffer^, FBufPos-FBuffer);
@ -362,6 +370,80 @@ begin
end;
end;
procedure TXMLWriter.WriteNSDef(B: TBinding);
begin
wrtChars(' xmlns', 6);
if B.Prefix^.Key <> '' then
begin
wrtChr(':');
wrtStr(B.Prefix^.Key);
end;
wrtChars('="', 2);
ConvWrite(B.uri, AttrSpecialChars, {$IFDEF FPC}@{$ENDIF}AttrSpecialCharCallback);
wrtChr('"');
end;
procedure TXMLWriter.NamespaceFixup(Element: TDOMElement);
var
B: TBinding;
i: Integer;
attr: TDOMNode;
s: DOMString;
action: TAttributeAction;
begin
FScratch.Count := 0;
if Element.hasAttributes then
begin
for i := 0 to Element.Attributes.Length-1 do
begin
attr := Element.Attributes[i];
if nfLevel2 in attr.Flags then
begin
if TDOMNode_NS(attr).NSI.NSIndex = 2 then
begin
if TDOMNode_NS(attr).NSI.PrefixLen = 0 then
s := ''
else
s := attr.localName;
FNSHelper.DefineBinding(s, attr.nodeValue, B);
if Assigned(B) then // drop redundant namespace declarations
VisitAttribute(attr);
end
else
FScratch.Add(attr);
end
else if TDOMAttr(attr).Specified then // Level 1 attribute
VisitAttribute(attr);
end;
end;
FNSHelper.DefineBinding(Element.Prefix, Element.namespaceURI, B);
if Assigned(B) then
WriteNSDef(B);
for i := 0 to FScratch.Count-1 do
begin
attr := TDOMNode(FScratch[i]);
action := FNSHelper.CheckAttribute(attr.Prefix, attr.namespaceURI, B);
if action = aaBoth then
WriteNSDef(B);
if action in [aaPrefix, aaBoth] then
begin
// use prefix from the binding, it might have been changed
wrtChr(' ');
wrtStr(B.Prefix^.Key);
wrtChr(':');
wrtStr(attr.localName);
wrtChars('="', 2);
// TODO: not correct w.r.t. entities
ConvWrite(attr.nodeValue, AttrSpecialChars, {$IFDEF FPC}@{$ENDIF}AttrSpecialCharCallback);
wrtChr('"');
end
else // action = aaUnchanged, output unmodified
VisitAttribute(attr);
end;
end;
procedure TXMLWriter.VisitElement(node: TDOMNode);
var
@ -371,10 +453,13 @@ var
begin
if not FInsideTextNode then
wrtIndent;
FNSHelper.StartElement;
wrtChr('<');
wrtStr(TDOMElement(node).TagName);
// FIX: Accessing Attributes was causing them to be created for every element :(
if node.HasAttributes then
if nfLevel2 in node.Flags then
NamespaceFixup(TDOMElement(node))
else if node.HasAttributes then
for i := 0 to node.Attributes.Length - 1 do
begin
child := node.Attributes.Item[i];
@ -402,6 +487,7 @@ begin
wrtStr(TDOMElement(Node).TagName);
wrtChr('>');
end;
FNSHelper.EndElement;
end;
procedure TXMLWriter.VisitText(node: TDOMNode);