Techninis straipsnis

ODS pivot lentelės Delphi: XML vardų srities apimtis

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

Ko pasigedo Pos pagrindu veikęs official-pivot.ods paėmimas HotXLS: pivot poimis neša loext ir calcext plėtinio atributus, o juos surišančios xmlns deklaracijos sėdi office:document-content šaknyje trisdešimt penkis susiejimus toliau, tad iškirptame fragmente kiekvienas naudojamas prefiksas liko nesurištas ir vardų sritis suprantantis analizatorius atmetė visą content.xml
Vardų srities deklaracija galioja nuo savo atidarančios iki uždirančios žymos, o iškirpus poimį iš dokumento jis iškertamas ir iš to ruožo, ir tada vienas atributas paverčia visą dalį neperskaitoma

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:

Kaip HotXLS v2.382.1 paima data pilot poimį kartu su jo vardų srities galiojimo ruožu: srautinis TXMLReader ėjimas laiko xmlns susiejimų steką, pažymėtą deklaravimo gyliu, ties table:data-pilot-tables taikiniu jį apeina nuo vidinio į išorę, gerbia šešėliavimą per Seen rinkinį, praleidžia prefiksus, kuriuos elementas paskelbia pats, ir išima susiejimus tiek uždirančiose žymose, tiek tuščiuose elementuose
Taikinio atpažinimas pagal kanoninį skaitytuvo vardą leidžia veikti ir tiems gamintojams, kurie perrašo table prefiksą, o poimis, kuris niekada neužsidaro, išmeta išimtį, o ne įrašo pusę fragmento atgal
// 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:

Kur paimtas pivot apibrėžimas atsiduria HotXLS ODS įrašyme: office:spreadsheet vaikai seka fiksuotą ODF seką nuo sugeneruotų table elementų per table:content-validations ir table:named-expressions, pažodinis table:data-pilot-tables fragmentas įsiterpia prieš table:database-ranges, o jokio API nėra, nes apibrėžimas keliauja kartu su OpenODS ir SaveAsODS
Pažodinio bloko negalima prikabinti ten, kur rašytojas tuo metu atsidūrė, o jo nešamos pirmtakų susiejimų kopijos nekenkia, nes Namespaces in XML leidžia iš naujo paskelbti prefiksą įdėtame ruože
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 Pos galiojimo 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 ar CDATA sekcijoje, 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