HotXLS Delphi Excel Component čuva OpenDocument data pilot tablice kroz ciklus otvaranja i spremanja ODS-a tako da pri otvaranju snimi podstablo <table:data-pilot-tables> iz content.xml doslovno i reproducira ga pri spremanju, od v2.382.0. Od v2.382.1 fragment nosi i svako XML namespace vezanje koje su deklarirali njegovi preci, pa spremljena definicija pivota ostaje dobro oblikovana za svakog potrošača, ne samo za HotXLS
Bug koji je iznudio obje promjene izašao je iz strogog corpus pokretanja. Uzorak official-pivot.ods, koji je napisao LibreOffice 6.1 development build, drži jedan pivot imenom DataPilot1 koji čita Sheet1.A2:E30 i smješta svoj rezultat u Sheet1.G6:J18. Otvorite ga HotXLS-om, spremite nepromijenjenog, prebrojite elemente <table:data-pilot-table> u izlazu: jedan unutra, nula van, i na Win32 i na Win64 jednako. Ništa u testu nije diralo pivot. Prvi krug probi usporedio je samo konstante ćelija i prošao; strukturna tvrdnja je ono što je razotkrilo gubitak, što je podsjetnik da je "vrijednosti se poklapaju" slaba definicija vjernosti round tripa
Zašto ODS pivot tablica nestane nakon spremanja u biblioteci?
ODS pivot tablica nestaje zato što HotXLS nema in-memory model za OpenDocument data pilot tablice, a ODS pisac gradi content.xml u cijelosti 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 generiran iz objekata koje radna knjiga doista drži. Definicija pivota — ODF 1.3 Part 3 §9.6, kontejner <table:data-pilot-tables> s po jednim <table:data-pilot-table> za svaki pivot, koji nosi svoj table:source-cell-range, svoju djecu table:data-pilot-field, svoj table:target-range-address i table:buttons — nema objekt u kojem bi živjela, pa je regenerirani dio jednostavno izostavlja
Kontrast s XLSX-om je namjeran. HotXLS parsira SpreadsheetML pivot cachee i pivot tablice u pravi model koji možete graditi, proširivati izračunatim poljima i osvježavati iz Delphija, pa oni preživljavaju spremanje jer se prepisuju, a ne kopiraju. ODS pivoti su puno rjeđi zahtjev, a modeliranje ODF data pilot vokabulara samo zbog round tripa bilo bi mnogo koda koji nitko ne uređuje. Pragmatičan odgovor isti je onaj koji HotXLS već primjenjuje na nepoznate extLst blokove u XLSX-u: sačuvaj ono što ne modeliraš, bajt po bajt ako možeš, događaj po događaj ako ne možeš
Što je prvo hvatanje temeljeno na Pos-u pogriješilo?
Hvatanje u v2.382.0 izrezalo je definiciju pivota iz content.xml kao običan string, a izrezani dio nije sadržavao deklaracije namespacea koje su ga činile smislenim. Implementacija je bila kratka kao što zvuči — dekodiraj dio u WideString, nađi početni tag s Pos, nađi završni tag nakon njega, kopiraj raspon u FRawOdsDataPilotTablesXml na radnoj knjizi:
// HotXLS v2.382.0 -- zamijenjeno 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); // cijeli 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 popravak je isporučen. Ono što ga je uhvatilo bila je druga, stroža provjera dodana istoga dana: svaki XML dio spremljenog paketa šalje se nezavisnom namespace-aware parseru izvan HotXLS-a, i taj parser odbio je novi content.xml s greškom nevezanog prefiksa. Pivot iz LibreOfficea nosi atribute proširenja proizvođača — loext:ignore-selected-page="true" na page polju, calcext:repeat-item-labels="false" na svakoj razini — a izrezani string sadržavao je te atribute ali ne i deklaracije xmlns:loext i xmlns:calcext koje su ih vezale. Te deklaracije sjedile su na korijenu <office:document-content> izvorne datoteke, njih trideset pet, dvije tisuće znakova daleko od pivota
W3C Namespaces in XML 1.0 §6.1 definira pravilo koje ovo čini tvrdim padom, a ne kozmetičkim: deklaracija namespacea u opsegu je od početnog taga elementa na kojem se pojavljuje do završnog taga tog elementa, i svako ime s prefiksom unutar tog opsega razrješava se prema njoj. Izrežite podstablo iz dokumenta i izrezali ste ga iz opsega. HotXLS piše vlastiti korijen <office:document-content> s jedanaest deklaracija — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo — pa se calcext: slučajno razriješio, table: se slučajno razriješio, a loext: nije. Namespace-aware parser tretira nevezani prefiks kao prekršaj dobro oblikovanosti, što znači da je cijeli dio nečitljiv, a ne samo jedan atribut
Kako HotXLS prenosi xmlns vezanja predaka na fragment?
HotXLS v2.382.1 zamijenio je izrezivanje stringa prolazom kroz content.xml vlastitim streaming TXMLReader-om, održavajući stack namespace vezanja označenih dubinom na kojoj je svako deklarirano, i kopirajući vezanja koja su još na snazi na korijenski element fragmenta u trenutku kada se stigne do cilja. Reader radi s uključenim PreserveWhitespaceText da tekstualni čvorovi dođu točno onako kako su napisani, a ponovno izgrađeni tagovi koriste TXMLReader.RawName i TXMLReader.Attribute[I].RawName — prefiks onako kako je napisan u datoteci — umjesto kanonskih imena koje reader inače predaje parserima dijelova. Ovo je srž petlje:
// Namespaces: TStringList od 'xmlns:p=uri' s dubinom deklariranja 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 djelotvorna vezanja predaka na korijen fragmenta.
for I := Namespaces.Count - 1 downto 0 do
begin
AttrName := WideString(Namespaces.Names[I]);
if Seen.IndexOf(String(AttrName)) >= 0 then Continue; // najnutarnije vezanje pobjeđuje
Seen.Add(String(AttrName));
if not Reader.HasAttribute(AttrName) then // već deklarirano ovdje? 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 ispravnost. Prolaženje stacka od najnutarnijeg vezanja prema van i pamćenje svakog prefiksa u Seen implementira zasjenjenje: ako bliži predak ponovno veže xmlns:table, bliža vrijednost pobjeđuje, točno kako §6.1 kaže da mora. Preskakanje prefiksa koje element već sam deklarira izbjegava emitiranje istog atributa dvaput, što bi bila druga vrsta prekršaja dobro oblikovanosti. A pravilo uklanjanja aktivira se na završnim tagovima i na praznim elementima, jer <x/> nikad ne proizvede EndElement događaj — ista zamka samozatvarajućeg elementa koju je hvatanje extLst-a u XLSX-u moralo naučiti. Uparivanje cilja po Reader.Name umjesto RawName tiša je pobjeda: reader kanonizira URI ODF table namespacea u prefiks table, pa proizvođač koji ga piše t:data-pilot-tables i dalje odgovara, dok emitirani fragment zadržava prefiks koji je proizvođač koristio
Petlja također odbija pogađati. Ako dio završi dok je hvatanje još otvoreno — odsječen ili neispravno oblikovan content.xml — OdsCaptureDataPilotTablesXml diže iznimku umjesto da vrati pola fragmenta, jer bi pola fragmenta bilo zapisano natrag pri spremanju i pretvorilo bi oštećen ulaz u oštećen izlaz s imenom biblioteke na sebi
Gdje fragment slijeće u spremljeni content.xml?
HotXLS piše snimljeni fragment u <office:spreadsheet> odmah nakon <table:named-expressions> koje generira i prije <table:database-ranges>. Content model <office:spreadsheet> iz ODF 1.3 Part 3 propisuje fiksni slijed za tu završnu djecu, pa doslovan blok ne može jednostavno biti dodan gdje god se pisac zatekne; mora se smjestiti u određeni utor. Sa strane pozivatelja nema API-ja niti bilo čega za konfiguriranje; definicija putuje uz obično otvaranje i spremanje:
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; // uređivanje unutar izvornog raspona pivota
Book.SaveAsODS('official-pivot-out.ods');
// content.xml u izlazu još nosi DataPilot1 sa svojim
// izvornim rasponom, poljima, ciljnim rasponom, gumbima i loext:/calcext: atributima
finally
Book.Free;
end;
end;
Redundantnost je namjerna i vrijedi je znati. Korijen fragmenta sada ponavlja xmlns:table i xmlns:calcext iako ih korijen spremljenog dokumenta također deklarira; Namespaces in XML dopušta ponovno deklariranje prefiksa u ugniježđenom opsegu, pa su duplikati bezopasni. Za LibreOffice uzorak preneseni skup su sve trideset pet root deklaracija, oko dva kilobajta povrh definicije od 8,357 znakova, jer hvatanje ne analizira koje prefikse podstablo doista koristi. Skeniranje iskorištenih prefiksa to bi smanjilo, i možda dođe kasnije; prvo ispravnost, pa kompaktnost
Pravilo za izrezivanje podstabala iz XML-a za doslovnu reprodukciju
Opća lekcija je da je podstablo samostalno tek kad ga takvim napravite, a namespace opseg je prva stvar koja puca kada to zaboravite. Kontrolna lista koju HotXLS sada primjenjuje na svako hvatanje tipa "sačuvaj ono što ne modeliramo":
- Prođite dokument pravim readerom i pratite vezanja u opsegu. Pretraga stringa s
Posuopće ne vidi opseg, a također promašuje na ugniježđenim elementima s istim imenom, na podudarnom stringu unutar komentara iliCDATAsekcije, i na vrijednostima atributa koje slučajno sadrže tekst taga - Kopirajte djelotvorna vezanja na korijen fragmenta, od najnutarnijeg prema van, jednom po prefiksu, preskačući ono što korijen već deklarira
- Zadržite prefiks onako kako je napisan u emitiranim tagovima; cilj uparujte po razriješenom namespaceu, a ne po doslovnom prefiksu
- Sačuvajte whitespace tekstualne čvorove i zapamtite da prazan element zatvara vlastiti opseg bez događaja završnog taga
- Validirajte spremljeni dio parserom koji nije biblioteka koja je testirana. Biblioteka će s veseljem ponovno pročitati vlastiti izlaz kroz istu blagu kodnu putanju koja ga je i napisala
Zadnja je točka ona koja je doista našla HXLS-003 drugi put. Prihvatna provjera u v2.382.0 bila je regularni izraz koji broji početne tagove data-pilot-table u spremljenom content.xml, a regularni izraz vidi tag, ne dokument — slijep je na to jesu li prefiksi na tom tagu vezani. Strogi corpus runner dodan u v2.382.1 parsira svaki XML i .rels dio spremljenog paketa namespace-aware parserom, a zatim uspoređuje stablo pivota — tag, sortirane atribute, tekst, djecu, rekurzivno — s originalom. Ta usporedba je namespace-expanded, pa bi preimenovanje prefiksa i dalje prošlo, a nevezani prefiks ne može
Gdje prestaje garancija doslovnosti
Dosljedna reprodukcija čuva definiciju; ona je ne razumije, i granice iz toga slijede. HotXLS ne izlaže nikakav API za čitanje, uređivanje ili osvježavanje ODS pivota, pa je FRawOdsDataPilotTablesXml interno polje i jedino vidljivo ponašanje je da definicija preživi. Fragment se ponovno serijalizira iz reader događaja, a ne kopira kao bajtovi: citiranje atributa i samozatvarajući oblici normaliziraju se, dok se tekst i whitespace zadržavaju. Snimljeni XML emitira samo ODS pisac sadržaja, pa radna knjiga otvorena iz .ods i spremljena kao .xlsx gubi pivot, a radna knjiga otvorena iz .xlsx nema što reproducirati u .ods spremanje — asimetrije ODS putanja uvoza i izvoza vrijede ovdje kao i svugdje. A budući da je definicija neprozirna, ne može pratiti vaše izmjene: preimenujte Sheet1 ili premjestite izvorne podatke u HotXLS-u i spremljeni pivot i dalje pokazuje na Sheet1.A2:E30, ostavljajući potrošaču da prijavi pokvaren raspon kad se sljedeći put osvježi. Jedna ograda o redoslijedu spada ovdje također: HotXLS emitira AutoFilter raspone kao <table:database-ranges> nakon fragmenta pivota, a corpus uzorak ne nosi nijedan database range, pa radnu knjigu s filtrom i pivotom treba provesti kroz ODF validator sheme prije nego što se pouzdate u relativni redoslijed tih dvaju elemenata
Testirajte s datotekama vlastitog proizvođača, ne samo s corpus uzorkom. Prijenos namespacea pokriva svaki prefiks koji proizvođač deklarira na pretku, ali dokument koji deklarira prefiks na samom elementu pivota, ili koji koristi zadani namespace za table vokabular, izvježbava grane preskakanja i zasjenjenja koje LibreOffice uzorak ne dotiče. Obje su implementirane; nijedna još nema uzorak u corpusu, a ta razlika je upravo vrsta stvari koju zapis u changelogu obično zamuti
Doslovno hvatanje data pilota u v2.382.0 i popravak namespace opsega u v2.382.1 isporučuju se u trenutnoj HotXLS Delphi Excel Component, čija stranica proizvoda navodi punu pokrivenost čitanja i pisanja ODS-a, XLSX-a i XLS-a za Delphi i C++Builder