HotXLS Delphi Excel Component čuva OpenDocument data pilot tabele kroz ciklus otvaranja i čuvanja ODS-a tako što pri otvaranju snima podstablo <table:data-pilot-tables> iz content.xml verbatim i reprodukuje ga pri čuvanju, i to od v2.382.0. Od v2.382.1 fragment nosi i svako XML namespace vezivanje koje su deklarisali njegovi preci, pa sačuvana definicija pivota ostaje dobro formirana za svakog potrošača, ne samo za HotXLS
Bug koji je iznudio obe izmene izašao je iz strogog corpus pokretanja. Uzorak official-pivot.ods, koji je napisala razvojna verzija LibreOffice-a 6.1, drži jedan pivot po imenu DataPilot1 koji čita Sheet1.A2:E30 i smešta rezultat u Sheet1.G6:J18. Otvorite ga u HotXLS-u, sačuvajte nepromenjenog, prebrojte elemente <table:data-pilot-table> u izlazu: jedan ulazi, nula izlazi, isto na Win32 i Win64. Ništa u testu nije diralo pivot. Prvi krug proba poredio je samo konstante ćelija i prošao; strukturna tvrdnja je ono što je razotkrilo gubitak, što je podsetnik da je „vrednosti se poklapaju" slaba definicija vernosti round trip-a
Zašto ODS pivot tabela nestane posle čuvanja iz biblioteke?
ODS pivot tabela nestane jer HotXLS nema model u memoriji za OpenDocument data pilot tabele, a ODS pisac gradi content.xml u potpunosti iz modela. Pisac sastavlja automatske stilove, po jedan <table:table> za svaki radni list, <table:content-validations>, <table:named-expressions> i <table:database-ranges>, svaki generisan iz objekata koje radna sveska zaista drži. Definicija pivota — ODF 1.3 Part 3 §9.6, kontejner <table:data-pilot-tables> sa po jednim <table:data-pilot-table> za svaki pivot, koji nosi svoj table:source-cell-range, svoju decu table:data-pilot-field, svoj table:target-range-address i table:buttons — nema objekat u kojem bi živela, pa je regenerisani deo jednostavno izostavlja
Kontrast sa XLSX-om je nameran. HotXLS parsira SpreadsheetML pivot caches i pivot tabele u pravi model koji možete da gradite, proširujete izračunatim poljima i osvežavate iz Delphi-ja, pa oni preživljavaju čuvanje jer se prepisuju, a ne kopiraju. ODS pivoti su mnogo ređi zahtev, a modelovanje ODF data pilot vokabulara samo zbog round trip-a bilo bi mnogo koda koji niko ne menja. Pragmatičan odgovor je isti onaj koji HotXLS već primenjuje na nepoznate extLst blokove u XLSX-u: sačuvaj ono što ne modeluješ, bajt za bajt ako možeš, događaj po događaj ako ne možeš
Šta je prvo hvatanje preko Pos-a pogrešilo?
Hvatanje u v2.382.0 iseklo je definiciju pivota iz content.xml kao običan string, a isečku su nedostajale deklaracije namespace-a koje su ga činile smislenim. Implementacija je bila kratka koliko i zvuči — dekodiraj deo u WideString, nađi otvarajući tag preko Pos, nađi zatvarajući tag posle njega, kopiraj raspon u FRawOdsDataPilotTablesXml na radnoj svesci:
// HotXLS v2.382.0 -- zamenjeno jedno izdanje kasnije
function OdsCaptureDataPilotTablesXml(Stream: TStream): WideString;
const
OpenTag: WideString = '<table:data-pilot-tables';
CloseTag: WideString = '</table:data-pilot-tables>';
var
Text: WideString;
StartPos, ClosePos: Integer;
begin
Result := '';
Text := LoadPartAsWideString(Stream); // ceo content.xml u memoriji
StartPos := Pos(OpenTag, Text);
if StartPos = 0 then Exit;
ClosePos := Pos(CloseTag, Copy(Text, StartPos, MaxInt));
if ClosePos = 0 then Exit;
Result := Copy(Text, StartPos, ClosePos + Length(CloseTag) - 1);
end;
Tvrdnja o broju postala je zelena, i ispravka je isporučena. Ono što ju je uhvatilo bila je druga, stroža provera dodata istog dana: svaki XML deo sačuvanog paketa prosleđuje se nezavisnom namespace-aware parseru izvan HotXLS-a, i taj parser odbio je novi content.xml greškom o nevezanom prefiksu. Pivot iz LibreOffice-a nosi producentske ekstenzijske atribute — loext:ignore-selected-page="true" na polju strane, calcext:repeat-item-labels="false" na svakom nivou — a isečeni string sadržao je te atribute ali ne i deklaracije xmlns:loext i xmlns:calcext koje su ih vezivale. Te deklaracije stajale su na korenu <office:document-content> izvornog fajla, trideset pet njih, dve hiljade znakova dalje od pivota
W3C Namespaces in XML 1.0 §6.1 definiše pravilo koje ovo čini tvrdim otkazom a ne kozmetičkim: deklaracija namespace-a je u opsegu od početnog taga elementa na kojem se pojavljuje do završnog taga tog elementa, i svako ime sa prefiksom unutar tog opsega razrešava se prema njoj. Isecite podstablo iz dokumenta i isekli ste ga iz opsega. HotXLS piše sopstveni koren <office:document-content> sa jedanaest deklaracija — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo — pa se calcext: slučajno razrešio, table: se slučajno razrešio, a loext: nije. Namespace-aware parser tretira nevezani prefiks kao kršenje dobro formiranosti, što znači da je ceo deo nečitljiv, a ne samo jedan atribut
Kako HotXLS prenosi xmlns vezivanja predaka na fragment?
HotXLS v2.382.1 zamenio je isečak stringa prolazom kroz content.xml preko sopstvenog streaming TXMLReader-a, održavajući stek namespace vezivanja označenih dubinom na kojoj je svako deklarisano, i kopirajući vezivanja koja su još na snazi na koreni element fragmenta u trenutku kada se stigne do cilja. Reader radi sa uključenim PreserveWhitespaceText tako da tekstualni čvorovi dolaze tačno onako kako su napisani, a rekonstruisani tagovi koriste TXMLReader.RawName i TXMLReader.Attribute[I].RawName — prefiks onako kako je napisan u fajlu — a ne kanonska imena koja reader inače predaje parserima delova. Evo jezgra petlje:
// Namespaces: TStringList sa 'xmlns:p=uri' i dubinom deklaracije u Objects[]
while Reader.Read do
begin
if CaptureDepth >= 0 then
XlsxAppendRawXmlReaderNode(Result, Reader); // element, tekst, CDATA, komentar
if Reader.NodeType = xmlntElement then
begin
for I := 0 to Reader.AttributeCount - 1 do
begin
AttrName := Reader.Attribute[I].RawName;
if (AttrName = 'xmlns') or (Pos(WideString('xmlns:'), AttrName) = 1) then
Namespaces.AddObject(String(AttrName) + '=' + String(Reader.Attribute[I].Value),
TObject(NativeInt(Depth)));
end;
if (CaptureDepth < 0) and (Reader.Name = 'table:data-pilot-tables') then
begin
Opening := XlsxRawXmlReaderOpenTag(Reader); // prvo skini završni '>' ili '/>'
...
// Prenesi efektivna vezivanja predaka na koren fragmenta.
for I := Namespaces.Count - 1 downto 0 do
begin
AttrName := WideString(Namespaces.Names[I]);
if Seen.IndexOf(String(AttrName)) >= 0 then Continue; // najunutrašnjije vezivanje pobeđuje
Seen.Add(String(AttrName));
if not Reader.HasAttribute(AttrName) then // već deklarisano ovde? preskoči
Opening := Opening + ' ' + AttrName + '="' +
XlsxEscapeAttr(WideString(Namespaces.ValueFromIndex[I])) + '"';
end;
...
CaptureDepth := Depth;
end;
if not Reader.IsEmptyElement then Inc(Depth);
end
else if Reader.NodeType = xmlntEndElement then
begin
Dec(Depth);
if Depth = CaptureDepth then Exit; // podstablo zatvoreno
end;
if (Reader.NodeType = xmlntEndElement) or
((Reader.NodeType = xmlntElement) and Reader.IsEmptyElement) then
while (Namespaces.Count > 0) and
(NativeInt(Namespaces.Objects[Namespaces.Count - 1]) >= Depth) do
Namespaces.Delete(Namespaces.Count - 1); // napusti opseg
end;
if CaptureDepth >= 0 then
raise Exception.Create('OpenDocument pivot definition ended inside an element');
Tri detalja u toj petlji nose korektnost. Prolazak kroz stek od najunutrašnjijeg vezivanja ka spolja uz pamćenje svakog prefiksa u Seen implementira shadowing: ako bliži predak ponovo veže xmlns:table, bliža vrednost pobeđuje, tačno kako §6.1 kaže da mora. Preskakanje prefiksa koje element već sam deklariše izbegava emitovanje istog atributa dvaput, što bi bila druga vrsta kršenja dobro formiranosti. A pravilo za skidanje sa steka pali se na završnim tagovima i na praznim elementima, jer <x/> nikad ne proizvede EndElement događaj — ista samo-zatvarajuća zamka koju je hvatanje extLst u XLSX-u moralo da nauči. Uparivanje cilja preko Reader.Name a ne RawName je tiša pobeda: reader kanonizuje ODF table namespace URI u prefiks table, pa producent koji ga piše kao t:data-pilot-tables i dalje prolazi, dok emitovani fragment zadržava prefiks koji je producent koristio
Petlja takođe odbija da pogađa. Ako se deo završi dok je hvatanje još otvoreno — skraćen ili malformiran content.xml — OdsCaptureDataPilotTablesXml baca izuzetak umesto da vrati pola fragmenta, jer bi pola fragmenta bilo upisano nazad pri čuvanju i pretvorilo bi oštećen ulaz u oštećen izlaz sa imenom biblioteke na sebi
Gde fragment sleće u sačuvanom content.xml?
HotXLS piše snimljeni fragment u <office:spreadsheet> odmah posle <table:named-expressions> koje generiše i pre <table:database-ranges>. Sadržajni model <office:spreadsheet> iz ODF 1.3 Part 3 propisuje fiksnu sekvencu za tu završnu decu, pa se verbatim blok ne može jednostavno dodati gde god se pisac zatekne; mora pasti u određeni slot. Sa strane pozivaoca nema API-ja i nema šta da se konfiguriše; definicija putuje uz obično otvaranje i čuvanje:
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
if Book.OpenODS('official-pivot.ods') <> 1 then
raise Exception.Create('open failed');
Book.Sheets[0].Cells[2, 5].Value := 1250.0; // izmena unutar izvornog opsega pivota
Book.SaveAsODS('official-pivot-out.ods');
// content.xml u izlazu i dalje nosi DataPilot1 sa svojim
// izvornim opsegom, poljima, target opsegom, dugmićima i loext:/calcext: atributima
finally
Book.Free;
end;
end;
Redundancija je namerna i vredi je znati. Koren fragmenta sada ponavlja xmlns:table i xmlns:calcext iako ih i koren sačuvanog dokumenta deklariše; Namespaces in XML dozvoljava ponovno deklarisanje prefiksa u ugnježdenom opsegu, pa su duplikati bezopasni. Za LibreOffice uzorak preneti skup su svih trideset pet root deklaracija, oko dva kilobajta povrh definicije od 8,357 znakova, jer hvatanje ne analizira koje prefikse podstablo zaista koristi. Sken korišćenih prefiksa to bi skratio, i možda dođe kasnije; korektnost prvo, kompaktnost drugo
Pravilo za isecanje podstabala iz XML-a za verbatim reprodukciju
Opšta pouka je da je podstablo samo-sadržano tek kada ga takvim napravite, a namespace opseg je prva stvar koja se polomi kada to zaboravite. Checklista koju HotXLS sada primenjuje na svako hvatanje u stilu „sačuvaj ono što ne modelujemo":
- Prolazite kroz dokument pravim reader-om i pratite vezivanja u opsegu. Pretraga stringa preko
Posne vidi opseg uopšte, a i promašuje kod ugnježdenih elemenata sa istim imenom, kod podudarnog stringa unutar komentara iliCDATAsekcije, i kod vrednosti atributa koje slučajno sadrže tekst taga - Kopirajte efektivna vezivanja na koren fragmenta, najunutrašnjije prvo, jednom po prefiksu, preskačući ono što koren već deklariše
- Zadržite prefiks onako kako je napisan u emitovanim tagovima; uparujte cilj po razrešenom namespace-u, ne po doslovnom prefiksu
- Sačuvajte tekstualne čvorove sa prazninama, i zapamtite da prazan element zatvara sopstveni opseg bez događaja završnog taga
- Validirajte sačuvani deo parserom koji nije biblioteka koja je pod testom. Biblioteka će sa zadovoljstvom ponovo pročitati sopstveni izlaz kroz istu blagu putanju koda koja ga je i napisala
Poslednja tačka je ona koja je zaista našla HXLS-003 drugi put. Prihvatna provera u v2.382.0 bila je regularni izraz koji broji početne tagove data-pilot-table u sačuvanom content.xml, a regularni izraz vidi tag, ne dokument — slep je za to da li su prefiksi na tom tagu vezani. Strogi corpus runner dodat u v2.382.1 parsira svaki XML i .rels deo sačuvanog paketa namespace-aware parserom i zatim poredi stablo pivota — tag, sortirane atribute, tekst, decu, rekurzivno — sa originalom. To poređenje je namespace-ekspandovano, pa bi preimenovanje prefiksa i dalje prošlo a nevezani prefiks ne može
Gde se garancija verbatim-a završava
Verbatim reprodukcija čuva definiciju; ona je ne razume, i granice iz toga slede. HotXLS ne izlaže nijedan API za čitanje, izmenu ili osvežavanje ODS pivota, pa je FRawOdsDataPilotTablesXml interno polje i jedino vidljivo ponašanje je da definicija preživi. Fragment se reserializuje iz reader događaja, ne kopira kao bajtovi: navodnici atributa i samo-zatvarajući oblici se normalizuju, dok se tekst i praznine čuvaju. Snimljeni XML emituje samo ODS pisac sadržaja, pa radna sveska otvorena iz .ods i sačuvana kao .xlsx gubi pivot, a radna sveska otvorena iz .xlsx nema šta da reprodukuje u .ods čuvanje — asimetrije ODS uvozne i izvozne putanje važe ovde kao i svuda. A pošto je definicija neprozirna, ne može da prati vaše izmene: preimenujte Sheet1 ili pomerite izvorne podatke u HotXLS-u i sačuvani pivot i dalje pokazuje na Sheet1.A2:E30, ostavljajući potrošaču da prijavi pokvaren opseg kada sledeći put osveži. Ovde pripada i jedno upozorenje o redosledu: HotXLS emituje AutoFilter opsege kao <table:database-ranges> posle fragmenta pivota, a corpus uzorak ne nosi nijedan database range, pa radnu svesku koja ima i filter i pivot vredi provući kroz ODF validator šeme pre nego što se pouzdate u relativni redosled ta dva elementa
Testirajte na fajlovima sopstvenog producenta, ne samo na corpus uzorku. Prenos namespace-a obrađuje svaki prefiks koji producent deklariše na pretku, ali dokument koji deklariše prefiks na samom elementu pivota, ili koji koristi podrazumevani namespace za table vokabular, vežba grane preskakanja i shadowing-a koje LibreOffice uzorak ne vežba. Obe su implementirane; nijedna još nema uzorak u corpus-u, a ta razlika je upravo ono što stavka u changelog-u obično zamuti
Verbatim hvatanje data pilot tabela u v2.382.0 i ispravka namespace opsega u v2.382.1 isporučuju se u aktuelnom HotXLS Delphi Excel Component, čija stranica proizvoda navodi punu ODS, XLSX i XLS read-write pokrivenost za Delphi i C++Builder