HotXLS Delphi Excel Component išlaiko OpenDocument data pilot lenteles per ODS atidarymo ir įrašymo ciklą: atidarydamas jis pažodžiui paima <table:data-pilot-tables> poimį iš content.xml ir atkuria jį įrašydamas — taip nuo v2.382.0. Nuo v2.382.1 fragmentas dar neša ir kiekvieną XML vardų srities susiejimą, kurį paskelbė jo pirmtakai, tad įrašytas pivot apibrėžimas lieka gerai suformuotas bet kuriam vartotojui, ne tik HotXLS
Klaida, privertusi abu pakeitimus, išlindo iš griežto korpuso paleidimo. Pavyzdyje official-pivot.ods, kurį parašė LibreOffice 6.1 kūrimo versija, yra vienas pivot, pavadintas DataPilot1, skaitantis Sheet1.A2:E30 ir dedantis rezultatą į Sheet1.G6:J18. Atidarykite jį su HotXLS, įrašykite nepakeistą, suskaičiuokite <table:data-pilot-table> elementus išvestyje: vienas įėjo, nulis išėjo — tiek Win32, tiek Win64. Teste niekas pivot nelietė. Pirmoji patikrų banga lygino tik ląstelių konstantas ir praėjo; struktūrinis teiginys ir atskleidė šį praradimą, o tai primena, kad „reikšmės sutampa" yra silpnas kelio pirmyn ir atgal ištikimumo apibrėžimas
Kodėl ODS pivot lentelė dingsta po bibliotekos įrašymo?
ODS pivot lentelė dingsta, nes HotXLS neturi OpenDocument data pilot lentelių modelio atmintyje, o ODS rašytojas content.xml surenka vien iš modelio. Rašytojas sudeda automatinius stilius, po vieną <table:table> kiekvienam darbalapiui, <table:content-validations>, <table:named-expressions> ir <table:database-ranges>, kiekvieną generuodamas iš objektų, kuriuos darbaknygė tikrai turi. Pivot apibrėžimas — ODF 1.3 3 dalies §9.6, <table:data-pilot-tables> konteineris su vienu <table:data-pilot-table> kiekvienam pivot, nešantis savo table:source-cell-range, savo table:data-pilot-field vaikus, savo table:target-range-address ir table:buttons — neturi kur gyventi, tad atkurta dalis jo tiesiog neįtraukia
Priešprieša su XLSX yra sąmoninga. HotXLS SpreadsheetML pivot cache ir pivot lenteles analizuoja į tikrą modelį, kurį galite kurti, plėsti skaičiuojamaisiais laukais ir atnaujinti iš Delphi, tad šie išlieka įrašant todėl, kad yra perrašomi, o ne kopijuojami. ODS pivot yra gerokai retesnis pageidavimas, o modeliuoti ODF data pilot žodyną vien dėl kelio pirmyn ir atgal būtų daug kodo, kurio niekas neredaguoja. Pragmatiškas atsakymas yra tas pats, kurį HotXLS jau taiko nežinomiems extLst blokams XLSX: laikyti tai, ko nemodeliuoji — baitas į baitą, jei įmanoma, įvykis į įvykį, jei ne
Ką sugadino pirmasis paėmimas su Pos?
v2.382.0 paėmimas pivot apibrėžimą iškirpo iš content.xml kaip paprastą eilutę, o iškirptame gabale trūko vardų srities deklaracijų, kurios jam suteikė prasmę. Įgyvendinimas buvo toks trumpas, kaip ir skamba: dekoduoti dalį į WideString, rasti atidarantį žymą su Pos, rasti po jos uždarantį žymą, nukopijuoti tą ruožą į darbaknygės FRawOdsDataPilotTablesXml:
// HotXLS v2.382.0 -- pakeista jau kitame leidime
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); // visas content.xml atmintyje
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;
Skaičiavimo teiginys tapo žalias ir pataisa išėjo. Ją pagavo tą pačią dieną pridėta antra, griežtesnė patikra: kiekviena įrašyto paketo XML dalis paduodama nepriklausomam, vardų sritis suprantančiam analizatoriui už HotXLS ribų, ir tas analizatorius atmetė naują content.xml su nesurišto prefikso klaida. LibreOffice pivot neša gamintojo plėtinio atributus — loext:ignore-selected-page="true" puslapio lauke, calcext:repeat-item-labels="false" kiekviename lygmenyje — o iškirptoje eilutėje tie atributai buvo, bet nebuvo juos surišančių xmlns:loext ir xmlns:calcext deklaracijų. Tos deklaracijos sėdėjo šaltinio failo <office:document-content> šaknyje — trisdešimt penkios iš jų — du tūkstančiai ženklų nuo pivot
W3C Namespaces in XML 1.0 §6.1 apibrėžia taisyklę, dėl kurios tai yra kietas gedimas, o ne kosmetinis: vardų srities deklaracija galioja nuo elemento, kuriame ji pasirodo, atidarančios žymos iki to elemento uždirančios žymos, ir kiekvienas prefiksuotas vardas tame galiojimo ruože sprendžiamas pagal ją. Iškirpkite poimį iš dokumento — ir iškirpsite jį iš to ruožo. HotXLS savo <office:document-content> šaknį rašo su vienuolika deklaracijų — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo — tad calcext: atsitiktinai išsisprendė, table: atsitiktinai išsisprendė, o loext: — ne. Vardų sritis suprantantis analizatorius nesurištą prefiksą laiko formos pažeidimu, o tai reiškia, kad visa dalis yra neperskaitoma, o ne vienas atributas
Kaip HotXLS perneša pirmtakų xmlns susiejimus į fragmentą?
HotXLS v2.382.1 pakeitė eilutės kirpimą ėjimu per content.xml su savo srautiniu TXMLReader, laikydamas vardų srities susiejimų steką, pažymėtą gyliu, kuriame kiekvienas buvo paskelbtas, ir tą akimirką, kai pasiekiamas taikinys, nukopijuoja dar galiojančius susiejimus į fragmento šakninį elementą. Skaitytuvas veikia su įjungtu PreserveWhitespaceText, kad teksto mazgai grįžtų lygiai tokie, kokie įrašyti, o atkurtos žymos naudoja TXMLReader.RawName ir TXMLReader.Attribute[I].RawName — prefikso rašybą iš failo — o ne kanoninius vardus, kuriuos skaitytuvas paprastai paduoda dalių analizatoriams. Štai ciklo branduolys:
// Namespaces: TStringList su 'xmlns:p=uri' ir deklaravimo gyliu Objects[]
while Reader.Read do
begin
if CaptureDepth >= 0 then
XlsxAppendRawXmlReaderNode(Result, Reader); // elementas, tekstas, CDATA, komentaras
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); // pirmiausia nubraukti '>' arba '/>'
...
// Pernešti galiojančius pirmtakų susiejimus į fragmento šaknį.
for I := Namespaces.Count - 1 downto 0 do
begin
AttrName := WideString(Namespaces.Names[I]);
if Seen.IndexOf(String(AttrName)) >= 0 then Continue; // laimi vidinis susiejimas
Seen.Add(String(AttrName));
if not Reader.HasAttribute(AttrName) then // jau paskelbta čia? praleisti
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; // poimis užsidarė
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); // išeiti iš galiojimo ruožo
end;
if CaptureDepth >= 0 then
raise Exception.Create('OpenDocument pivot definition ended inside an element');
Trys to ciklo detalės laiko teisingumą. Steko ėjimas nuo vidinio susiejimo į išorę ir kiekvieno prefikso įsiminimas Seen rinkinyje įgyvendina šešėliavimą: jei artimesnis pirmtakas iš naujo susieja xmlns:table, laimi artimesnė reikšmė, lygiai kaip ir reikalauja §6.1. Prefiksų, kuriuos elementas jau pats paskelbia, praleidimas padeda neišvesti to paties atributo du kartus, o tai būtų jau kitas formos pažeidimas. O išėmimo taisyklė suveikia ir uždirančiose žymose, ir tuščiuose elementuose, nes <x/> niekada nesukuria EndElement įvykio — tos pačios savaiminio užsidarymo spąstų, kurių turėjo išmokti XLSX extLst paėmimas. Taikinio atpažinimas pagal Reader.Name, o ne RawName, yra tylesnė pergalė: skaitytuvas ODF lentelės vardų srities URI kanonizuoja į table prefiksą, tad gamintojas, kuris jį rašo t:data-pilot-tables, vis tiek atpažįstamas, o išvestame fragmente lieka toks prefiksas, kokį gamintojas ir naudojo
Ciklas taip pat atsisako spėlioti. Jei dalis baigiasi, kai paėmimas dar atviras — nukirptas ar sugadintas content.xml — OdsCaptureDataPilotTablesXml išmeta išimtį, o ne grąžina pusę fragmento, nes pusė fragmento būtų įrašyta atgal ir paverstų sugadintą įvestį sugadinta išvestimi su bibliotekos vardu
Kur fragmentas atsiduria įrašytame content.xml?
HotXLS paimtą fragmentą įrašo į <office:spreadsheet> iškart po savo sugeneruoto <table:named-expressions> ir prieš <table:database-ranges>. ODF 1.3 3 dalies <office:spreadsheet> turinio modelis tiems paskutiniams vaikams numato fiksuotą seką, tad pažodinio bloko negalima tiesiog prikabinti ten, kur rašytojas tuo metu atsidūrė; jis turi būti įdėtas į konkrečią vietą. Iš kvietėjo pusės jokio API ir nieko konfigūruoti nėra; apibrėžimas keliauja kartu su paprastu atidarymu ir įrašymu:
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; // pakeitimas pivot šaltinio srities viduje
Book.SaveAsODS('official-pivot-out.ods');
// Išvesties content.xml vis dar neša DataPilot1 su jo
// šaltinio sritimi, laukais, taikinio sritimi, mygtukais ir loext:/calcext: atributais
finally
Book.Free;
end;
end;
Pertekliškumas yra sąmoningas ir vertas žinojimo. Fragmento šaknis dabar kartoja xmlns:table ir xmlns:calcext, nors įrašyto dokumento šaknis juos irgi paskelbia; Namespaces in XML leidžia iš naujo paskelbti prefiksą įdėtame ruože, tad dublikatai nekenkia. LibreOffice pavyzdžio atveju pernešamas rinkinys yra visos trisdešimt penkios šaknies deklaracijos — apie du kilobaitus ant 8357 ženklų apibrėžimo — nes paėmimas neanalizuoja, kuriuos prefiksus poimis tikrai naudoja. Naudojamų prefiksų nuskaitymas tą sutrumpintų, ir galbūt jis ateis vėliau; pirmiausia teisingumas, paskui kompaktiškumas
Taisyklė, kaip iškirpti poimius iš XML pažodiniam atkūrimui
Bendra pamoka tokia: poimis yra savarankiškas tik tada, kai jūs jį tokį padarote, o vardų srities galiojimas yra pirmas dalykas, kuris sulūžta, kai to pamirštate. Kontrolinis sąrašas, kurį HotXLS dabar taiko bet kuriam „laikyti tai, ko nemodeliuojame" paėmimui:
- Eikite per dokumentą tikru skaitytuvu ir sekite galiojančius susiejimus. Paieška eilutėje su
Posgaliojimo ruožo nemato apskritai, o be to klysta ir dėl įdėtų elementų tuo pačiu pavadinimu, ir dėl sutampančios eilutės komentare arCDATAsekcijoje, ir dėl atributo reikšmių, kuriose atsitiktinai yra žymos tekstas - Nukopijuokite galiojančius susiejimus į fragmento šaknį, nuo vidinio į išorę, po vieną kartą kiekvienam prefiksui, praleisdami tai, ką šaknis jau paskelbia
- Išvestose žymose laikykite neapdorotą prefikso rašybą; taikinį atpažinkite pagal išspręstą vardų sritį, o ne pagal pažodinį prefiksą
- Išsaugokite tarpų teksto mazgus ir atminkite, kad tuščias elementas savo galiojimo ruožą uždaro be uždirančios žymos įvykio
- Tikrinkite įrašytą dalį analizatoriumi, kuris nėra testuojama biblioteka. Biblioteka mielai perskaitys savo pačios išvestį tuo pačiu atlaidžiu kodu, kuriuo ją ir parašė
Paskutinis punktas ir yra tas, kuris antrą kartą tikrai surado HXLS-003. v2.382.0 priėmimo patikra buvo reguliarioji išraiška, skaičiuojanti data-pilot-table atidarančias žymas įrašytame content.xml, o reguliarioji išraiška mato žymą, o ne dokumentą — ji akla tam, ar tos žymos prefiksai surišti. Griežtas korpuso leidėjas, pridėtas v2.382.1, kiekvieną įrašyto paketo XML ir .rels dalį analizuoja vardų sritis suprantančiu analizatoriumi ir paskui lygina pivot medį — žymą, surikiuotus atributus, tekstą, vaikus, rekursyviai — su originalu. Tas palyginimas išplėstas vardų sritimis, tad prefikso perrašymas vis tiek praeitų, o nesurištas prefiksas — ne
Kur baigiasi pažodinė garantija
Pažodinis atkūrimas apibrėžimą išsaugo; jis jo nesupranta, ir ribos iš to išplaukia. HotXLS neatidengia jokio API ODS pivot skaityti, redaguoti ar atnaujinti, tad FRawOdsDataPilotTablesXml yra vidinis laukas, o vienintelis pastebimas elgesys — kad apibrėžimas išlieka. Fragmentas iš naujo serializuojamas iš skaitytuvo įvykių, o ne kopijuojamas kaip baitai: atributų kabutės ir savaiminio užsidarymo formos normalizuojamos, o tekstas ir tarpai išsaugomi. Paimtas XML išvedamas tik ODS turinio rašytojo, tad darbaknygė, atidaryta iš .ods ir įrašyta kaip .xlsx, pivot praranda, o darbaknygė, atidaryta iš .xlsx, neturi ko atkurti į .ods įrašymą — ODS importo ir eksporto kelių asimetrijos galioja čia kaip ir visur. O kadangi apibrėžimas nepermatomas, jis negali sekti jūsų pakeitimų: pervadinkite Sheet1 arba perkelkite šaltinio duomenis HotXLS ir įrašytas pivot vis tiek nurodys į Sheet1.A2:E30, palikdamas vartotojui pranešti apie sulūžusią sritį, kai jis kitą kartą atsinaujins. Vienas eiliškumo įspėjimas čia taip pat priklauso: HotXLS AutoFilter sritis išveda kaip <table:database-ranges> po pivot fragmento, o korpuso pavyzdys neturi jokios database range, tad darbaknygę su filtru ir pivot kartu vertėtų praleisti per ODF schemos tikrintuvą, prieš pasikliaujant santykine tų dviejų elementų tvarka
Testuokite su savo gamintojo failais, o ne vien su korpuso pavyzdžiu. Vardų sričių pernešimas susitvarko su bet kuriuo prefiksu, kurį gamintojas paskelbia pirmtake, bet dokumentas, paskelbiantis prefiksą pačiame pivot elemente arba naudojantis numatytąją vardų sritį lentelės žodynui, paliečia praleidimo ir šešėliavimo šakas, kurių LibreOffice pavyzdys nepasiekia. Abi įgyvendintos; nė viena dar neturi pavyzdžio korpuse, ir tas skirtumas yra būtent toks dalykas, kokį changelog įrašas linkęs nuslėpti
Pažodinis data pilot paėmimas v2.382.0 ir vardų srities apimties pataisa v2.382.1 platinami dabartiniame HotXLS Delphi Excel Component, kurio produkto puslapyje išvardyta visa ODS, XLSX ir XLS skaitymo bei rašymo aprėptis Delphi ir C++Builder