HotXLS Delphi Excel Component, OpenDocument data pilot tablolarını bir ODS aç-kaydet döngüsü boyunca koruyor: açma anında content.xml dosyasının <table:data-pilot-tables> alt ağacını birebir yakalıyor ve kayıtta yeniden oynatıyor; bu davranış v2.382.0'dan beri böyle. v2.382.1'den beri fragman, üst öğelerinin bildirdiği her XML namespace bağını da taşıyor, böylece kaydedilen pivot tanımı yalnızca HotXLS için değil her tüketici için well-formed kalıyor
Her iki değişikliği zorunlu kılan hata, sıkı bir corpus koşusundan çıktı. Bir LibreOffice 6.1 geliştirme derlemesiyle yazılmış official-pivot.ods örneği, Sheet1.A2:E30 okuyup sonucunu Sheet1.G6:J18 alanına yazan DataPilot1 adlı tek bir pivot içeriyor. HotXLS ile açın, değiştirmeden kaydedin, çıktıdaki <table:data-pilot-table> öğelerini sayın: giren bir, çıkan sıfır; hem Win32 hem Win64 için böyle. Testte hiçbir şey pivot'a dokunmuyordu. İlk sorgu turu yalnızca hücre sabitlerini karşılaştırmış ve geçmişti; kaybı ortaya çıkaran yapısal assertion oldu — bu da değerler uyuşuyor ifadesinin round-trip sadakati için zayıf bir tanım olduğunu hatırlatıyor
Bir ODS pivot tablosu kütüphane kaydından sonra neden kayboluyor?
Bir ODS pivot tablosu kayboluyor, çünkü HotXLS'in OpenDocument data pilot tabloları için bellekte bir modeli yok ve ODS yazıcısı content.xml dosyasını tamamen modelden kuruyor. Yazıcı otomatik stilleri, her worksheet için bir <table:table>, <table:content-validations>, <table:named-expressions> ve <table:database-ranges> öğelerini, her biri workbook'un gerçekten taşıdığı nesnelerden üretilecek biçimde bir araya getiriyor. Bir pivot tanımının — ODF 1.3 Part 3 §9.6, pivot başına bir <table:data-pilot-table> içeren <table:data-pilot-tables> kabı; beraberinde table:source-cell-range, table:data-pilot-field çocukları, table:target-range-address ve table:buttons — yaşayacağı bir nesne yok, bu yüzden yeniden üretilen parça onu basitçe atlıyor
XLSX ile aradaki karşıtlık bilinçli. HotXLS, SpreadsheetML pivot cache'lerini ve pivot tablolarını Delphi'den kurup hesaplanmış alanlarla genişletebileceğiniz ve yenileyebileceğiniz gerçek bir modele ayrıştırır; bunlar kayıttan sağ çıkar çünkü kopyalanmaz, yeniden yazılırlar. ODS pivot'ları çok daha seyrek gelen bir istek ve yalnızca round-trip uğruna ODF data pilot sözlüğünü modellemek, kimsenin düzenlemediği bir sürü kod demek olurdu. Pragmatik yanıt, HotXLS'in XLSX'teki bilinmeyen extLst bloklarına zaten uyguladığı yanıtın aynısı: modellemediğini koru; yapabiliyorsan bayt bayt, yapamıyorsan olay olay
İlk Pos tabanlı yakalama neyi yanlış yaptı?
v2.382.0'daki yakalama, pivot tanımını content.xml içinden düz bir string olarak kesip çıkarıyordu ve kesilen parça, tanımı anlamlı kılan namespace bildirimlerini içermiyordu. Uygulama, kulağa geldiği kadar kısaydı: parçayı bir WideString olarak çöz, açılış etiketini Pos ile bul, kapanış etiketini ondan sonra ara, aralığı workbook üzerindeki FRawOdsDataPilotTablesXml alanına kopyala:
// HotXLS v2.382.0 -- bir sürüm sonra geçersiz kaldı
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); // content.xml'in tamamı bellekte
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;
Sayım assertion'ı yeşile döndü ve düzeltme yayınlandı. Onu yakalayan şey aynı gün eklenen ikinci ve daha sıkı bir kontrol oldu: kaydedilen paketin her XML parçası HotXLS dışındaki bağımsız, namespace farkında bir parser'a veriliyor ve o parser yeni content.xml dosyasını bağlanmamış prefix hatasıyla reddetti. LibreOffice'ten gelen pivot, üretici uzantı öznitelikleri taşıyor — bir sayfa alanında loext:ignore-selected-page="true", her düzeyde calcext:repeat-item-labels="false" — ve kesilen string bu öznitelikleri içeriyordu ama onları bağlayan xmlns:loext ile xmlns:calcext bildirimlerini içermiyordu. O bildirimler kaynak dosyanın <office:document-content> kökünde duruyordu; otuz beş tanesi, pivot'tan iki bin karakter uzakta
W3C Namespaces in XML 1.0 §6.1, bunu kozmetik değil katı bir hata yapan kuralı tanımlar: bir namespace bildirimi, bulunduğu öğenin başlangıç etiketinden o öğenin bitiş etiketine kadar kapsam içindedir ve o kapsam içindeki her prefix'li ad ona göre çözümlenir. Bir alt ağacı belgeden kesip çıkarırsanız onu kapsamın dışına da çıkarırsınız. HotXLS kendi <office:document-content> kökünü on bir bildirimle yazıyor — office, table, text, style, number, fo, draw, svg, xlink, calcext, tableooo — bu yüzden calcext: şans eseri çözümlendi, table: şans eseri çözümlendi, loext: ise çözümlenemedi. Namespace farkında bir parser, bağlanmamış bir prefix'i well-formedness ihlali sayar; bu da tek bir özniteliğin değil, parçanın tamamının okunamaz olduğu anlamına gelir
HotXLS üst öğelerin xmlns bağlarını fragmana nasıl taşıyor?
HotXLS v2.382.1, string kesme işleminin yerine content.xml üzerinde kendi streaming TXMLReader'ıyla bir geçiş koydu; her bağın bildirildiği derinlikle etiketlenmiş bir namespace bağı yığını tutuyor ve hedefe ulaşıldığı anda hâlâ yürürlükte olan bağları fragmanın kök öğesine kopyalıyor. Reader, PreserveWhitespaceText etkin olarak çalışıyor, böylece metin düğümleri yazıldığı gibi geri geliyor; yeniden kurulan etiketler ise reader'ın parça ayrıştırıcılarına normalde verdiği kanonik adlar yerine TXMLReader.RawName ve TXMLReader.Attribute[I].RawName değerlerini, yani dosyadaki prefix yazımını kullanıyor. Döngünün çekirdeği şöyle:
// Namespaces: 'xmlns:p=uri' girdilerini tutan TStringList; bildiren derinlik Objects[] içinde
while Reader.Read do
begin
if CaptureDepth >= 0 then
XlsxAppendRawXmlReaderNode(Result, Reader); // öğe, metin, CDATA, yorum
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); // önce sondaki '>' ya da '/>' kırpılır
...
// Yürürlükteki üst öğe bağlarını fragman köküne taşı.
for I := Namespaces.Count - 1 downto 0 do
begin
AttrName := WideString(Namespaces.Names[I]);
if Seen.IndexOf(String(AttrName)) >= 0 then Continue; // en içteki bağ kazanır
Seen.Add(String(AttrName));
if not Reader.HasAttribute(AttrName) then // burada zaten bildirilmiş mi? atla
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; // alt ağaç kapandı
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); // kapsamdan çık
end;
if CaptureDepth >= 0 then
raise Exception.Create('OpenDocument pivot definition ended inside an element');
Bu döngüde doğruluğu üç ayrıntı taşıyor. Yığını en içteki bağdan dışa doğru yürümek ve her prefix'i Seen içinde hatırlamak gölgelemeyi uygular: daha yakın bir üst öğe xmlns:table bağını yeniden bildirirse yakın olan değer kazanır, tam da §6.1'in gerektirdiği gibi. Öğenin kendi bildirdiği prefix'leri atlamak aynı özniteliğin iki kez yazılmasını önler; bu da başka bir well-formedness hatası olurdu. Çıkarma kuralı ise bitiş etiketlerinde ve boş öğelerde tetiklenir, çünkü <x/> hiçbir zaman bir EndElement olayı üretmez — XLSX extLst yakalamasının da öğrenmek zorunda kaldığı aynı self-closing tuzağı. Hedefi RawName yerine Reader.Name ile eşleştirmek daha sessiz bir kazanç: reader, ODF table namespace URI'sini table prefix'ine kanonikleştiriyor, böylece onu t:data-pilot-tables diye yazan bir üretici de eşleşiyor; yazılan fragman ise üreticinin kullandığı prefix'i koruyor
Döngü ayrıca tahmin yürütmeyi reddediyor. Yakalama hâlâ açıkken parça biterse — kesilmiş ya da bozuk bir content.xml — OdsCaptureDataPilotTablesXml yarım bir fragman döndürmek yerine istisna fırlatıyor; çünkü yarım fragman kayıtta geri yazılır ve hasarlı bir girdiyi, üzerinde kütüphanenin adı olan hasarlı bir çıktıya dönüştürürdü
Fragman kaydedilen content.xml içinde nereye yerleşiyor?
HotXLS, yakalanan fragmanı <office:spreadsheet> içine, ürettiği <table:named-expressions> öğesinden hemen sonra ve <table:database-ranges> öğesinden önce yazıyor. <office:spreadsheet> öğesinin ODF 1.3 Part 3 içerik modeli bu sondaki çocuklar için sabit bir sıra öngörür, yani birebir bir blok yazıcının o an bulunduğu yere eklenemez; belirli bir yuvaya bırakılması gerekir. Çağıran tarafında hiçbir API ve yapılandırılacak hiçbir şey yok; tanım sıradan bir aç-kaydet ile birlikte geliyor:
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; // pivot kaynak aralığının içinde düzenleme
Book.SaveAsODS('official-pivot-out.ods');
// çıktıdaki content.xml hâlâ DataPilot1'i şunlarla birlikte taşıyor:
// kaynak aralığı, alanlar, hedef aralık, buttons ve loext:/calcext: öznitelikleri
finally
Book.Free;
end;
end;
Bu fazlalık bilinçli ve bilinmeye değer. Fragman kökü artık xmlns:table ve xmlns:calcext bağlarını, kaydedilen belge kökü de onları bildirse bile tekrarlıyor; Namespaces in XML bir prefix'in iç içe kapsamda yeniden bildirilmesine izin verir, bu yüzden tekrarlar zararsız. LibreOffice örneği için taşınan küme otuz beş kök bildirimin tamamı, yani 8.357 karakterlik tanımın üzerine yaklaşık iki kilobayt; çünkü yakalama, alt ağacın hangi prefix'leri gerçekten kullandığını analiz etmiyor. Kullanılan prefix'leri taramak bunu kırpardı ve belki ileride gelir; önce doğruluk, sonra kompaktlık
Birebir replay için XML'den alt ağaç kesme kuralı
Genel ders şu: bir alt ağaç ancak siz onu kendi kendine yeterli hâle getirdikten sonra kendi kendine yeterlidir ve unuttuğunuzda ilk kırılan şey namespace kapsamıdır. HotXLS'in artık modellemediğini koru türünden her yakalamaya uyguladığı kontrol listesi:
- Belgeyi gerçek bir reader ile dolaşın ve kapsamdaki bağları takip edin.
Posile string arama kapsamı hiç göremez; ayrıca aynı adlı iç içe öğelerde, bir yorum ya daCDATAbölümü içindeki eşleşen string'de ve etiket metnini içeren öznitelik değerlerinde de yanlış eşleşir - Yürürlükteki bağları fragman köküne kopyalayın; en içtekinden başlayarak, prefix başına bir kez ve kökün zaten bildirdiklerini atlayarak
- Yazılan etiketlerde ham prefix yazımını koruyun; hedefi harfi harfine prefix ile değil, çözümlenmiş namespace ile eşleştirin
- Whitespace metin düğümlerini koruyun ve boş bir öğenin kendi kapsamını bitiş etiketi olayı olmadan kapattığını unutmayın
- Kaydedilen parçayı, test edilen kütüphanenin kendisi olmayan bir parser ile doğrulayın. Kütüphane kendi çıktısını, onu yazan aynı hoşgörülü kod yolundan memnuniyetle yeniden okur
Asıl önemli olan son madde; HXLS-003'ü ikinci kez bulan da o oldu. v2.382.0 kabul kontrolü, kaydedilen content.xml içindeki data-pilot-table başlangıç etiketlerini sayan bir düzenli ifadeydi ve düzenli ifade bir etiketi görür, bir belgeyi değil: o etiketteki prefix'lerin bağlı olup olmadığına kördür. v2.382.1'de eklenen sıkı corpus koşucusu, kaydedilen paketin her XML ve .rels parçasını namespace farkında bir parser ile ayrıştırıp pivot ağacını — etiket, sıralanmış öznitelikler, metin, çocuklar, özyinelemeli olarak — orijinalle karşılaştırır. Bu karşılaştırma namespace açılımlıdır, dolayısıyla prefix'in yeniden yazımı yine geçer, bağlanmamış bir prefix ise geçemez
Birebir garanti nerede bitiyor
Birebir replay bir tanımı korur; onu anlamaz ve sınırlar da bundan çıkar. HotXLS bir ODS pivot'unu okumak, düzenlemek ya da yenilemek için hiçbir API sunmuyor, bu yüzden FRawOdsDataPilotTablesXml dahili bir alan ve gözlemlenebilir tek davranış tanımın hayatta kalması. Fragman bayt olarak kopyalanmaz, reader olaylarından yeniden serileştirilir: öznitelik tırnaklaması ve self-closing biçimler normalleştirilir, metin ve whitespace ise korunur. Yakalanan XML yalnızca ODS içerik yazıcısı tarafından yazılır; yani .ods olarak açılıp .xlsx olarak kaydedilen bir workbook pivot'u kaybeder, .xlsx olarak açılan bir workbook'un ise bir .ods kaydına oynatacak hiçbir şeyi yoktur — ODS içe ve dışa aktarma yollarının asimetrileri burada da her yerde olduğu gibi geçerli. Tanım opak olduğu için düzenlemelerinizi de takip edemez: HotXLS içinde Sheet1 adını değiştirin ya da kaynak veriyi taşıyın, kaydedilen pivot hâlâ Sheet1.A2:E30 adresini gösterir ve bir sonraki yenilemede bozuk aralığı bildirme işi tüketiciye kalır. Bir sıralama uyarısı da buraya ait: HotXLS AutoFilter aralıklarını pivot fragmanından sonra <table:database-ranges> olarak yazıyor ve corpus örneği hiçbir database range taşımıyor; bu yüzden hem filtre hem pivot içeren bir workbook'u, bu iki öğenin göreli sırasına güvenmeden önce bir ODF şema doğrulayıcıdan geçirmek gerekir
Yalnızca corpus örneğiyle değil, kendi üreticinizin dosyalarıyla da test edin. Namespace taşıma, bir üreticinin üst öğede bildirdiği her prefix'i karşılar; ama prefix'i pivot öğesinin kendisinde bildiren ya da table sözlüğü için varsayılan namespace kullanan bir belge, LibreOffice örneğinin tetiklemediği atlama ve gölgeleme dallarını çalıştırır. İkisi de uygulandı; ikisinin de corpus'ta henüz bir örneği yok ve bu ayrım tam da bir changelog girdisinin bulanıklaştırmaya meyilli olduğu türden bir şey
v2.382.0'daki birebir data pilot yakalaması ve v2.382.1'deki namespace kapsamı düzeltmesi güncel HotXLS Delphi Excel Component içinde geliyor; ürün sayfası Delphi ve C++Builder için tam ODS, XLSX ve XLS okuma-yazma kapsamını listeliyor