HotXLS Delphi Excel Component ohranja pivot tabele OpenDocument skozi cikel odpiranja in shranjevanja ODS tako, da ob odpiranju dobesedno zajame poddrevo <table:data-pilot-tables> v content.xml in ga ob shranjevanju predvaja — od različice 2.382.0. Od različice 2.382.1 fragment nosi tudi vsako vezavo imenskega prostora XML, ki so jo razglasili njegovi predniki, zato shranjena definicija pivot ostane dobro oblikovana za vsakega porabnika in ne le za HotXLS
Hrošč, ki je vsilil obe spremembi, je prišel iz strogega zagona korpusa. Vzorec official-pivot.ods, ki ga je zapisala razvojna različica LibreOffice 6.1, vsebuje en pivot z imenom DataPilot1, ki bere Sheet1.A2:E30 in svoj rezultat odloži v Sheet1.G6:J18. Odprite ga s HotXLS, shranite nespremenjenega, preštejte elemente <table:data-pilot-table> v izhodu: en noter, nič ven, enako na Win32 in Win64. Noben preskus se pivota ni dotaknil. Prvi krog sond je primerjal le konstante celic in šel skozi; strukturna trditev je tista, ki je izgubo razkrila, kar je opomnik, da je "vrednosti se ujemajo" šibka definicija zvestobe round-tripa
Zakaj pivot tabela ODS po shranjevanju iz knjižnice izgine?
Pivot tabela ODS izgine, ker HotXLS za pivot tabele OpenDocument nima modela v pomnilniku, pisec ODS pa content.xml zgradi v celoti iz modela. Pisec sestavi samodejne stile, eno <table:table> na delovni list, <table:content-validations>, <table:named-expressions> in <table:database-ranges> — vsakega iz objektov, ki jih delovni zvezek res vsebuje. Definicija pivot — ODF 1.3 3. del §9.6, vsebnik <table:data-pilot-tables> z enim <table:data-pilot-table> na pivot, ki nosi svoj table:source-cell-range, svoje otroke table:data-pilot-field, svoj table:target-range-address in table:buttons — nima objekta, v katerem bi živela, zato jo znova zgrajeni del preprosto izpusti
Nasprotje z XLSX je namerno. HotXLS razčleni predpomnilnike in pivot tabele SpreadsheetML v pravi model, ki ga lahko zgradite, razširite z izračunanimi polji in osvežite iz Delphija, zato ti shranjevanje preživijo, ker se prepišejo in ne kopirajo. Pivoti ODS so veliko redkejša zahteva in modeliranje besednjaka data pilot ODF samo zaradi round-tripa bi bilo veliko kode, ki je nihče ne ureja. Pragmatični odgovor je isti, kot ga HotXLS že uporablja za neznane bloke extLst v XLSX: kar ne modelirate, obdržite — bajt za bajtom, če gre, dogodek za dogodkom, če ne gre
Kaj je prvi zajem na osnovi Pos naredil narobe?
Zajem v različici 2.382.0 je definicijo pivot izrezal iz content.xml kot navaden niz, izrezku pa so manjkale razglasitve imenskih prostorov, ki so mu dajale pomen. Izvedba je bila tako kratka, kot se sliši — dekodiraj del v WideString, najdi začetno oznako s Pos, za njo najdi končno oznako, kopiraj obseg v FRawOdsDataPilotTablesXml na delovnem zvezku:
// HotXLS v2.382.0 -- nadomeščeno eno različico pozneje
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); // celoten content.xml v pomnilniku
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;
Trditev o štetju je postala zelena in popravek je izšel. Ujel ga je drugi, strožji pregled, dodan isti dan: vsak del XML v shranjenem paketu se pošlje neodvisnemu razčlenjevalniku, ki pozna imenske prostore in deluje zunaj HotXLS, ta razčlenjevalnik pa je novi content.xml zavrnil z napako nevezane predpone. Pivot iz LibreOffice nosi atribute razširitev proizvajalca — loext:ignore-selected-page="true" na polju strani, calcext:repeat-item-labels="false" na vsaki ravni — in izrezani niz je vseboval te atribute, ne pa razglasitev xmlns:loext in xmlns:calcext, ki sta jih vezali. Ti razglasitvi sta sedeli na korenu <office:document-content> izvorne datoteke, petintrideset jih je bilo, dva tisoč znakov stran od pivota
W3C Namespaces in XML 1.0 §6.1 določa pravilo, ki iz tega naredi trdo napako in ne kozmetične: razglasitev imenskega prostora je v obsegu od začetne oznake elementa, na katerem se pojavi, do končne oznake tega elementa, vsako ime s predpono znotraj tega obsega pa se razreši proti njej. Izrežite poddrevo iz dokumenta in izrezali ste ga tudi iz obsega. HotXLS zapiše svoj lasten koren <office:document-content> z enajstimi razglasitvami — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo — zato se je calcext: slučajno razrešil, table: se je slučajno razrešil, loext: pa ne. Razčlenjevalnik, ki pozna imenske prostore, nevezano predpono obravnava kot kršitev dobre oblikovanosti, kar pomeni, da je cel del neberljiv in ne le en atribut
Kako HotXLS prenese vezave xmlns prednikov na fragment?
HotXLS 2.382.1 je nizovni izrez nadomestil s prehodom čez content.xml skozi svoj pretočni TXMLReader, pri čemer vzdržuje sklad vezav imenskih prostorov, označenih z globino, na kateri je bila vsaka razglašena, in v trenutku, ko doseže cilj, kopira vezave, ki so še v veljavi, na korenski element fragmenta. Bralnik teče z omogočenim PreserveWhitespaceText, da se besedilna vozlišča vrnejo natanko tako, kot so bila zapisana, znova zgrajene oznake pa uporabljajo TXMLReader.RawName in TXMLReader.Attribute[I].RawName — črkovanje predpone iz datoteke — in ne kanoničnih imen, ki jih bralnik običajno izroči razčlenjevalnikom delov. Tu je jedro zanke:
// Namespaces: TStringList z 'xmlns:p=uri', globina razglasitve v Objects[]
while Reader.Read do
begin
if CaptureDepth >= 0 then
XlsxAppendRawXmlReaderNode(Result, Reader); // element, besedilo, 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); // najprej odstrani končni '>' ali '/>'
...
// Prenesi veljavne vezave prednikov 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; // najgloblja vezava zmaga
Seen.Add(String(AttrName));
if not Reader.HasAttribute(AttrName) then // že razglašeno tukaj? 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; // poddrevo zaprto
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); // zapusti obseg
end;
if CaptureDepth >= 0 then
raise Exception.Create('OpenDocument pivot definition ended inside an element');
Tri podrobnosti v tej zanki nosijo pravilnost. Hoja po skladu od najgloblje vezave navzven z zapomnitvijo vsake predpone v Seen izvede senčenje: če bližnji prednik znova razglasi xmlns:table, obvelja bližnja vrednost, natanko tako, kot zahteva §6.1. Preskok predpon, ki jih element razglasi sam, se izogne dvojnemu oddajanju istega atributa, kar bi bila druga kršitev dobre oblikovanosti. Pravilo odlaganja pa se sproži ob končnih oznakah in ob praznih elementih, ker <x/> nikoli ne proizvede dogodka EndElement — ista past samozapirajočih elementov, ki se je je moral naučiti zajem extLst v XLSX. Ujemanje cilja po Reader.Name in ne po RawName je tišja pridobitev: bralnik kanonizira URI imenskega prostora tabele ODF v predpono table, zato se proizvajalec, ki jo zapiše kot t:data-pilot-tables, še vedno ujame, oddani fragment pa obdrži predpono, kakršno je uporabil proizvajalec
Zanka tudi noče ugibati. Če se del konča, medtem ko je zajem še odprt — odrezan ali pokvarjen content.xml — OdsCaptureDataPilotTablesXml sproži izjemo, namesto da bi vrnil polovičen fragment, ker bi se polovičen fragment ob shranjevanju zapisal nazaj in poškodovan vnos spremenil v poškodovan izhod s podpisom knjižnice
Kam fragment pristane v shranjenem content.xml?
HotXLS shranjeni fragment zapiše v <office:spreadsheet> takoj za <table:named-expressions>, ki jih ustvari, in pred <table:database-ranges>. Vsebinski model <office:spreadsheet> iz ODF 1.3 3. dela predpisuje fiksno zaporedje teh zadnjih otrok, zato dobesednega bloka ni mogoče preprosto pripeti, kjer se pisec trenutno znajde; vstaviti ga je treba na točno določeno mesto. S strani klicatelja ni nobenega API-ja in ničesar za nastaviti; definicija se pelje skupaj z običajnim odpiranjem in shranjevanjem:
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; // urejanje znotraj izvornega obsega pivota
Book.SaveAsODS('official-pivot-out.ods');
// content.xml v izhodu še vedno nosi DataPilot1 s svojim
// izvornim obsegom, polji, ciljnim obsegom, gumbi in atributi loext:/calcext:
finally
Book.Free;
end;
end;
Podvajanje je namerno in vredno vedenja. Koren fragmenta zdaj ponovi xmlns:table in xmlns:calcext, čeprav ju razglasi tudi koren shranjenega dokumenta; Namespaces in XML dovoljuje ponovno razglasitev predpone v gnezdenem obsegu, zato so dvojniki neškodljivi. Pri vzorcu LibreOffice je preneseni nabor vseh petintrideset korenskih razglasitev, približno dva kilobajta povrhu definicije z 8357 znaki, ker zajem ne analizira, katere predpone poddrevo res uporablja. Pregled uporabljenih predpon bi to skrčil in morda pride pozneje; najprej pravilnost, šele nato strnjenost
Pravilo za rezanje poddreves iz XML za dobesedno predvajanje
Splošna lekcija je, da je poddrevo samozadostno šele, ko ga takega naredite, obseg imenskih prostorov pa je prva stvar, ki se zlomi, ko to pozabite. Kontrolni seznam, ki ga HotXLS zdaj uporablja za vsak zajem v smislu "obdrži, česar ne modeliramo":
- Dokument prehodite s pravim bralnikom in sledite vezavam v obsegu. Iskanje po nizu s
Posobsega sploh ne vidi, zgreši pa tudi pri gnezdenih elementih z istim imenom, pri ujemajočem se nizu v komentarju ali odsekuCDATAin pri vrednostih atributov, ki slučajno vsebujejo besedilo oznake - Kopirajte veljavne vezave na koren fragmenta, od najgloblje navzven, enkrat na predpono, s preskokom tistega, kar koren že razglasi
- V oddanih oznakah obdržite izvirno črkovanje predpone; cilj ujemajte po razrešenem imenskem prostoru in ne po dobesedni predponi
- Ohranite besedilna vozlišča z belino in ne pozabite, da prazen element zapre svoj obseg brez dogodka končne oznake
- Shranjeni del preverite z razčlenjevalnikom, ki ni knjižnica, ki jo preskušate. Knjižnica bo svoj izhod z veseljem znova prebrala skozi isto popustljivo pot kode, ki ga je zapisala
Zadnja točka je tista, ki je HXLS-003 dejansko našla drugič. Sprejemni pregled v različici 2.382.0 je bil regularni izraz, ki je štel začetne oznake data-pilot-table v shranjenem content.xml, regularni izraz pa vidi oznako in ne dokumenta — slep je za to, ali so predpone na tej oznaki vezane. Strogi izvajalnik korpusa, dodan v različici 2.382.1, razčleni vsak del XML in .rels v shranjenem paketu s preizkuševalnikom, ki pozna imenske prostore, nato pa drevo pivota — oznaka, urejeni atributi, besedilo, otroci, rekurzivno — primerja z izvirnikom. Ta primerjava je razširjena po imenskih prostorih, zato bi drugačno črkovanje predpone še vedno šlo skozi, nevezana predpona pa nikakor
Kje se jamstvo dobesednosti konča
Dobesedno predvajanje definicijo ohrani; ne razume je, in meje izhajajo iz tega. HotXLS ne izpostavlja nobenega API-ja za branje, urejanje ali osveževanje pivota ODS, zato je FRawOdsDataPilotTablesXml notranje polje in edino opazno vedenje je, da definicija preživi. Fragment se znova serializira iz dogodkov bralnika in se ne kopira kot bajti: črkovanje atributov in samozapirajoče oblike se normalizirajo, besedilo in belina pa se ohranita. Zajeti XML oddaja samo pisec vsebine ODS, zato delovni zvezek, odprt iz .ods in shranjen kot .xlsx, pivot izgubi, delovni zvezek, odprt iz .xlsx, pa nima česa predvajati v shranjevanje .ods — nesimetrije poti uvoza in izvoza ODS veljajo tu kot povsod. In ker je definicija neprozorna, ne more slediti vašim urejanjem: preimenujte Sheet1 ali premaknite izvorne podatke v HotXLS in shranjeni pivot bo še vedno kazal na Sheet1.A2:E30, porabnik pa bo ob naslednji osvežitvi prijavil pokvarjen obseg. Sem sodi tudi eno opozorilo glede vrstnega reda: HotXLS oddaja obsege AutoFilter kot <table:database-ranges> za fragmentom pivota, vzorec korpusa pa ne nosi nobenega obsega podatkovne baze, zato delovni zvezek s filtrom in pivotom poženite skozi preverjevalnik sheme ODF, preden se zanesete na relativni vrstni red teh dveh elementov
Preskušajte z datotekami svojega proizvajalca in ne le z vzorcem korpusa. Prenos imenskih prostorov obdela vsako predpono, ki jo proizvajalec razglasi na predniku, dokument, ki predpono razglasi na samem elementu pivota ali uporabi privzeti imenski prostor za besednjak tabele, pa preizkusi veji preskoka in senčenja, ki jih vzorec LibreOffice ne. Obe sta izvedeni; nobena še nima vzorca v korpusu, in prav ta razlika je tisto, kar vnos v changelogu rad zabriše
Dobesedni zajem pivot tabel iz različice 2.382.0 in popravek obsega imenskih prostorov iz različice 2.382.1 izhajata v trenutni komponenti HotXLS Delphi Excel Component, katere stran izdelka navaja celotno pokritost branja in pisanja ODS, XLSX in XLS za Delphi in C++Builder