مقال تقني

جولة جدول pivot في ODS لدلفي: نطاق مساحات أسماء XML

يحفظ مكوّن HotXLS لدلفي لمكون Excel جداول data pilot في OpenDocument عبر دورة فتح وحفظ لـ ODS بملتقط الشجرة الجزئية <table:data-pilot-tables> من content.xml حرفيًا عند الفتح وإعادة إظهارها عند الحفظ، منذ v2.382.0. ومنذ v2.382.1 تحمل الشظية أيضًا كل ارتباط من ارتباطات مساحات أسماء XML أعلنه أسلافها، فيبقى تعريف pivot المحفوظ مكتمل الشكل لأي مستهلك، لا لـ HotXLS وحده

الخطأ الذي فرض التغييرين خرج من تشغيل صارم للمجموعة. العينة official-pivot.ods، كتبتها نسخة تطوير من LibreOffice 6.1، تحمل pivot واحدًا اسمه DataPilot1 يقرأ Sheet1.A2:E30 ويهبط بنتيجته في Sheet1.G6:J18. افتحه بـ HotXLS، واحفظه دون تعديل، وعُدّ عناصر <table:data-pilot-table> في الناتج: واحد داخل، صفر خارج، على Win32 وWin64 سواء. لم يلمس الاختبار الـ pivot. الجولة الأولى من الفحوص قارنت ثوابت الخلايا فقط واجتازت؛ والتأكيد البنيوي هو ما كشف الفقد، وهو تذكير بأن «القيم متطابقة» تعريف ضعيف لأمانة الجولة

لماذا يختفي جدول pivot في ODS بعد حفظ المكتبة؟

يختفي جدول pivot في ODS لأن HotXLS لا يملك نموذجًا في الذاكرة لجداول data pilot في OpenDocument، ولأن كاتب ODS يبني content.xml من النموذج كليًا. يجمع الكاتب الأنماط التلقائية، و<table:table> واحدة لكل ورقة عمل، و<table:content-validations>، و<table:named-expressions>، و<table:database-ranges>، كلًا منها مولدًا من كائنات يحملها المصنف فعلًا. تعريف pivot — ODF 1.3 Part 3 §9.6، حاوية <table:data-pilot-tables> فيها <table:data-pilot-table> لكل pivot، تحمل table:source-cell-range وأبناء table:data-pilot-field وtable:target-range-address وtable:buttons — لا يجد كائنًا يسكنه، فيحذفه الجزء المعاد توليده ببساطة

المفارقة مع XLSX مقصودة. يحلل HotXLS مخابئ pivot وجداول pivot في SpreadsheetML إلى نموذج حقيقي تستطيع بناءه وتوسعته بحقول محسوبة وتحديثه من دلفي، فتنجو تلك عبر الحفظ لأنها يعاد كتابتها لا نسخها. محاور pivot في ODS طلب نادر الحدوث كثيرًا، ونمذجة مفردات data pilot في ODF من أجل الجولة وحدها قدر كبير من الكود لن يحرره أحد. الجواب العملي هو نفسه الذي يطبقه HotXLS أصلًا على كتل extLst المجهولة في XLSX: احفظ ما لا تمثله، بايتًا ببايت إن استطعت، وحدثًا بحدث إن لم تستطع

ماذا أخطأ الالتقاط الأول المبني على Pos؟

شذّب الالتقاط في v2.382.0 تعريف pivot من content.xml بوصفه سلسلة عادية، وكانت الشذبة ناقصة إعلانات مساحات الأسماء التي تجعلها ذات معنى. كان التنفيذ قصيرًا كما يبدو — فك ترميز الجزء إلى WideString، ابحث عن وسم الفتح بـ Pos، ابحث عن وسم الإغلاق بعده، وانسخ المدى إلى FRawOdsDataPilotTablesXml على المصنف:

// HotXLS v2.382.0 -- حل محله إصدار لاحق
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 كله في الذاكرة
  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;

اشتبك تأكيد العدّ، وشُحن الإصلاح. ما اصطاده فحص ثانٍ أكثر صرامة أضيف في اليوم نفسه: كل جزء XML من الحزمة المحفوظة يُغذى إلى محلل مستقل واعٍ بمساحات الأسماء خارج HotXLS، ورفض ذلك المحلل content.xml الجديد بخطأ بادئة غير مرتبطة. يحمل pivot القادم من LibreOffice سمات امتداد المنتج — loext:ignore-selected-page="true" على حقل صفحة، وcalcext:repeat-item-labels="false" على كل مستوى — وكانت السلسلة المقطوعة تحوي تلك السمات لكنها لا تحوي إعلاني xmlns:loext وxmlns:calcext اللذين يربطانهما. كانت تلك الإعلانات جاثمة على جذر <office:document-content> في الملف المصدر، خمسة وثلاثين إعلانًا، على بعد ألفي محرف من الـ pivot

يحدد W3C Namespaces in XML 1.0 §6.1 القاعدة التي تجعل هذا فشلًا قاطعًا لا تجميليًا: إعلان مساحة الاسم داخل النطاق من وسم فتح العنصر الذي يظهر عليه إلى وسم إغلاقه، وكل اسم مسبوق داخل ذلك النطاق يُحل ضده. اقطع شجرة جزئية من المستند وستقطعها من النطاق. يكتب HotXLS جذر <office:document-content> الخاص به بأحد عشر إعلانًا — office وtable وtext وstyle وnumber وfo وdraw وsvg وxlink وcalcext وtableooo — فصدف أن calcext: تحل، وصدف أن table: تحل، ولم تحل loext:. يعامل المحلل الواعي بمساحات الأسماء البادئة غير المرتبطة خرقًا لاكتمال الشكل، أي أن الجزء كله غير قابل للقراءة، لا سمة واحدة منه

ما فاته الالتقاط المبني على Pos لعينة official-pivot.ods في HotXLS: تحمل شجرة pivot الجزئية سمات الامتداد loext وcalcext بينما تجثم إعلانات xmlns الرابطة لهما على جذر office:document-content على بعد خمسة وثلاثين ارتباطًا، فخرجت الشظية المقطوعة تاركة كل بادئة استخدمتها بلا ربط، ورفض المحلل الواعي بمساحات الأسماء content.xml كله
إعلان مساحة الاسم داخل النطاق من وسم فتحه إلى وسم إغلاقه، وقطع شجرة جزئية من المستند يقطعها من ذلك النطاق، فيتحول خطأ سمة واحدة إلى جزء غير قابل للقراءة

كيف يحمل HotXLS ارتباطات xmlns للأجداد إلى الشظية؟

استبدل HotXLS v2.382.1 شذبة السلسلة بمرور على content.xml عبر TXMLReader التدفقية الخاصة به، محافظًا على رصين من ارتباطات مساحات الأسماء موسوم بعمق إعلان كل واحد، وينسخ الارتباطات ما تزال فاعلة على عنصر جذر الشظية لحظة بلوغ الهدف. يعمل القارئ وفق PreserveWhitespaceText مفعلة فتعود عقد النص كما كُتبت تمامًا، وتستخدم الوسوم المعاد بناؤها TXMLReader.RawName وTXMLReader.Attribute[I].RawName — تهجئة البادئة من الملف — لا الأسماء القيانية التي يسلمها القارئ عادةً لمحللي الأجزاء. هذا جوهر الحلقة:

كيف يلتقط HotXLS v2.382.1 شجرة data pilot الجزئية مع نطاق مساحات أسمائها: يحفظ مرور TXMLReader التدفقي رصينًا من ارتباطات xmlns موسومة بعمق الإعلان، ويمر عليه من الأعمق أولًا عند هدف table:data-pilot-tables، ويحترم التظليل عبر مجموعة Seen، ويتخطى البادئات التي يعلنها العنصر نفسه، ويصفّر الارتباطات عند وسوم النهاية والعناصر الفارغة سواء
مطابقة الهدف بالاسم القياني للقارئ تبقي المنتجات التي تعيد تهجئة بادئة table عاملة، والشجرة الجزئية التي لا تُغلق ترفع استثناء بدل كتابة نصف شظية عند الحفظ
// Namespaces: قائمة TStringList من 'xmlns:p=uri' مع عمق الإعلان في Objects[]
while Reader.Read do
begin
  if CaptureDepth >= 0 then
    XlsxAppendRawXmlReaderNode(Result, Reader);   // عنصر، نص، CDATA، تعليق
  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);   // انزع '>' أو '/>' الختامية أولًا
      ...
      // انقل ارتباطات الأجداد الفاعلة إلى جذر الشظية.
      for I := Namespaces.Count - 1 downto 0 do
      begin
        AttrName := WideString(Namespaces.Names[I]);
        if Seen.IndexOf(String(AttrName)) >= 0 then Continue;   // الأدنى عمقًا يفوز
        Seen.Add(String(AttrName));
        if not Reader.HasAttribute(AttrName) then               // معلن هنا أصلًا؟ تخطَّ
          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;                           // الشجرة الجزئية أُغلقت
  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);                   // غادر النطاق
end;
if CaptureDepth >= 0 then
  raise Exception.Create('OpenDocument pivot definition ended inside an element');

ثلاثة تفاصيل في تلك الحلقة تحمل الصحة. السير على الرصين من الارتباط الأعمق نحو الخارج مع حفظ كل بادئة في Seen ينفذ التظليل: إذا أعاد سلف أقرب ربط xmlns:table فالقيمة الأقرب تفوز، تمامًا كما يشترط §6.1. وتخطي البادئات التي يعلنها العنصر بنفسه يجنب إصدار السمة نفسها مرتين، وهو خطأ اكتمال شكل مختلف. وقاعدة الصفّر تعمل عند وسوم النهاية وكذلك عند العناصر الفارغة، لأن <x/> لا تنتج أبدًا حدث EndElement — الفخ ذاتي الإغلاق نفسه الذي اضطر إلى تعلمه التقاط extLst في XLSX. ومطابقة الهدف بـ Reader.Name لا بـ RawName مكسب أهدأ: يقيّن القارئ URI لمساحة اسم ODF table إلى البادئة table، فيظل منتج يتهجئها t:data-pilot-tables مطابقًا، بينما تحتفظ الشظية الصادرة بأي بادئة استخدمها المنتج

ويرفض الحلقة أيضًا أن تخمّن. إذا انتهى الجزء والالتقاط ما يزال مفتوحًا — content.xml مبتور أو مشوه — يرفع OdsCaptureDataPilotTablesXml استثناء بدل إعادة نصف شظية، لأن نصف شظية سيُكتب عند الحفظ فيحول مدخلًا متضررًا إلى ناتج متضرر يحمل اسم المكتبة

أين تهبط الشظية في content.xml المحفوظ؟

يكتب HotXLS الشظية الملتقطة داخل <office:spreadsheet> بعد <table:named-expressions> التي يولدها مباشرة وقبل <table:database-ranges>. يصف نمط محتوى ODF 1.3 Part 3 لـ <office:spreadsheet> تتابعًا ثابتًا لأولاد الختام أولئك، فلا يمكن ببساطة إلحاق كتلة حرفية حيثما صادف الكاتب؛ بل يجب إنزالها في خانة محددة. ومن جهة المستدعي لا توجد واجهة برمجية ولا شيء للضبط؛ التعريف ينتقل مع فتح وحفظ عاديين:

أين يهبط تعريف pivot الملتقط في حفظ HotXLS لـ ODS: يتبع أولاد office:spreadsheet تتابع ODF الثابت من عناصر الجدول المولدة عبر table:content-validations وtable:named-expressions، وتتسند شظية table:data-pilot-tables الحرفية قبل table:database-ranges، ولا توجد واجهة برمجية لأن التعريف ينتقل مع OpenODS وSaveAsODS
كتلة حرفية لا يمكن إلحاقها حيثما صادف الكاتب، ونسخ ارتباطات الأجداد التي تحملها غير مؤذية لأن Namespaces in XML يسمح بإعادة إعلان بادئة في نطاق متداخل
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
    Book.SaveAsODS('official-pivot-out.ods');
    // content.xml في الناتج ما يزال يحمل DataPilot1 مع
    // نطاق مصدره وحقوله ونطاقه الهدف وأزراره وسمات loext:/calcext:
  finally
    Book.Free;
  end;
end;

الوفرة مقصودة وتستحق المعرفة. يكرر جذر الشظية الآن xmlns:table وxmlns:calcext حتى لو كان جذر المستند المحفوظ يعلنهما أيضًا؛ يسمح Namespaces in XML بإعادة إعلان بادئة في نطاق متداخل، فالتكرارات غير مؤذية. لعينة LibreOffice المجموعة المنقولة هي إعلانات الجذر الخمسة والثلاثين كلها، نحو كيلوبايتين فوق التعريف ذي 8,357 محرفًا، لأن الالتقاط لا يحلل أي بادئات تستخدمها الشجرة الجزئية فعلًا. مسح البادئات المستخدمة سيقص ذلك، وقد يأتي لاحقًا؛ الصحة أولًا والاختصار ثانيًا

قاعدة لقص أشجار جزئية من XML لإعادة إظهار حرفي

الدرس العام أن الشجرة الجزئية لا تصبح مكتفية بذاتها إلا حين تجعلها كذلك، ونطاق مساحة الاسم أول ما ينكسر حين تنسى. القائمة التي يطبقها HotXLS الآن على أي التزام «احفظ ما لا نمثله»:

  • امش المستند بقارئ حقيقي وتتبع الارتباطات داخل النطاق. البحث بالسلاسل عبر Pos لا يرى النطاق أصلًا، وهو كذلك يخطئ المطابقة في عناصر متداخلة بالاسم نفسه، وفي سلسلة مطابقة داخل تعليق أو قسم CDATA، وفي قيم سمات تصادف أن تحوي نص الوسم
  • انسخ الارتباطات الفاعلة إلى جذر الشظية، الأعمق أولًا، مرة واحدة لكل بادئة، متخطيًا ما يعلنه الجذر أصلًا
  • احفظ تهجئة البادئة الخام في الوسوم الصادرة؛ وطابق الهدف بمساحة الاسم المحلولة لا بالبادئة الحرفية
  • احفظ عقد نص المسافات البيضاء، وتذكر أن العنصر الفارغ يغلق نطاقه بنفسه دون حدث وسم نهاية
  • دقق الجزء المحفوظ بمحلل ليس هو المكتبة قيد الاختبار. ستقرأ المكتبة ناتجها بكل سرور عبر مسار الكود المتساهل نفسه الذي كتبه

النقطة الأخيرة هي التي وجدت HXLS-003 في المرة الثانية فعلًا. كان فحص القبول في v2.382.0 تعبيرًا نمطيًا يعد وسوم فتح data-pilot-table في content.xml المحفوظ، والتعبير النمطي يرى وسمًا لا مستندًا — إنه أعمى عن كون بادئات ذلك الوسم مرتبطة أم لا. مشغل المجموعة الصارم المضاف في v2.382.1 يحلل كل جزء XML وكل .rels من الحزمة المحفوظة بمحلل واعٍ بمساحات الأسماء ثم يقارن شجرة الـ pivot — الوسم والسمات مرتبة والنص والأولاد، تكراريًا — بالأصل. تلك المقارنة موسعة بمساحات الأسماء، فإعادة تهجئة بادئة ما تزال تجتاز والبادئة غير المرتبطة لا تجتاز

أين ينتهي ضمان الحرفية

إعادة الإظهار الحرفية تحفظ تعريفًا؛ لا تفهمه، والحدود تتبع من ذلك. لا يكشف HotXLS أي واجهة لقراءة أو تحرير أو تحديث pivot في ODS، فـ FRawOdsDataPilotTablesXml حقل داخلي والسلوك الوحيد الملحوظ أن التعريف ينجو. تعاد تسلسل الشظية من أحداث القارئ، لا نسخًا بالبايتات: اقتباس السمات وصيغ الإغلاق الذاتي تُطبَّع، بينما يُحفظ النص والمسافات. XML الملتقط يصدره كاتب محتوى ODS فقط، فمصنف فُتح من .ods وحُفظ بـ .xlsx يفقد الـ pivot، ومصنف فُتح من .xlsx لا يملك ما يعيد إظهاره في حفظ .ods — لا تماثل مساري استيراد وتصدير ODS ينطبق هنا كما في كل مكان. ولأن التعريف معتم، لا يستطيع أن يتبع تعديلاتك: أعد تسمية Sheet1 أو انقل بيانات المصدر في HotXLS وسيبقى الـ pivot المحفوظ يشير إلى Sheet1.A2:E30، تاركًا للمستهلك أن يبلغ عن مدى مكسور عند تحديثه التالي. وتنبيه ترتيبي يخص هذا الموضع أيضًا: يصدر HotXLS مديات AutoFilter بوصفها <table:database-ranges> بعد شظية الـ pivot، وعينة المجموعة لا تحمل مدى قاعدة بيانات، فمصنف فيه مرشح وpivot معًا يجب أن يمر عبر مدقق مخطط ODF قبل أن تعتمد على الترتيب النسبي لهذين العنصرين

اختبر بملفات منتجك أنت، لا بعينة المجموعة فقط. نقل مساحات الأسماء يعالج أي بادئة يعلنها منتج على سلف، لكن مستندًا يعلن بادئة على عنصر الـ pivot نفسه، أو يستخدم مساحة أسماء افتراضية لمفردات الجدول، يمارس فروع التخطي والتظليل التي لا تمارسها عينة LibreOffice. كلاهما منفذ؛ ولا شيء منهما له عينة في المجموعة بعد، وهذا التمييز بالضبط من الأشياء التي تميل مدخلات سجل التغييرات إلى طمسها

الالتقاط الحرفي لجداول data pilot في v2.382.0 وإصلاح نطاق مساحات الأسماء في v2.382.1 يأتيان في مكوّن HotXLS لدلفي لمكون Excel الحالي، وتسرد صفحة المنتج تغطية القراءة والكتابة الكاملة لـ ODS وXLSX وXLS لدلفي وC++Builder