Откройте книгу, сохранённую LibreOffice Calc, и колонка, записанная как hidden="false", может вернуться скрытой, хотя в файле всё правильно. Под xsd:boolean — типом за этим атрибутом — легальны true, false, 1 и 0, с окружающими пробелами или без; HotXLS для Delphi читает каждое из этих написаний везде, где схема XLSX объявляет булево
Половина xsd:boolean, которую Excel никогда не пишет
W3C XML Schema Part 2 §3.2.2 определяет boolean четырьмя легальными литералами — true, false, 1 и 0 — и фиксирует фасетку whiteSpace в collapse, так что customHeight=" 1 " так же валиден, как customHeight="1". Та же секция называет true и false каноническими формами. SpreadsheetML это ничуть не сужает: sml.xsd объявляет customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix и десятки их соседей обычным xsd:boolean, большинство с явным дефолтом false, а некоторые, как showGridLines, с дефолтом true. Excel по привычке пишет только неканонические цифры. LibreOffice и некоторые другие производители пишут слова, поэтому пакет из Calc несёт <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> там, где Excel записал бы customHeight="1"
Именно эта асимметрия позволяет односимвольным сравнениям годами жить в читателях OOXML. Соберите тестовый корпус из вывода Excel, и каждый булев, какой вы там увидите, будет 0 или 1. Сравнение проходит все тесты. А потом пользователю попадает файл Calc. Кладите хотя бы несколько пакетов, сохранённых LibreOffice, в любой корпус XLSX, на котором гоняются тесты читателя, — его написание булевых одно из нескольких мест, где производители расходятся, та же щель, что вылезает в формулах и правилах, когда вы переносите книги между Excel и LibreOffice через ODS. Для дизайна парсера важна ещё одна деталь типа: литералы чувствительны к регистру, так что TRUE вообще не валидный xsd:boolean, — это вспомните, когда будете решать, насколько снисходительным быть вашему собственному читателю
Что не так у сравнения в один символ
Читатель, сравнивающий атрибут с одной цифрой, проваливается в ту или другую сторону. В какую — зависит от дефолта, который сравнение молча в себе несёт. Таблица перечисляет формы, встречающиеся в рукописном OOXML-коде, что каждая делает с написаниями-словами и несколько атрибутов, где ущерб виден. За кадром остался showDropDown, чья беда в имени, а не в написании: дефолт схемы — false, но showDropDown="1" на валидации списка прячет стрелку в ячейке, поэтому TXLSXDataValidation.ShowDropDown наружу выставляет человеческий смысл (True показывает стрелку) и хранит инвертированный флаг OOXML
| Наивная проверка | Подразумеваемый дефолт | Ошибочно читает | Где проявляется |
|---|---|---|---|
Value = '1' | false | true становится false | sheetFormatPr customHeight, <v> ячейки t="b" |
Value <> '0' | true при наличии | false становится true | колонка hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false игнорируется, дефолт остаётся включённым | sheetView showGridLines, переключатели разрешений sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true обрезается до t | границы data-table ChartML, smooth серии |
Средняя строка даёт самый заметный ущерб: hidden="false" на элементе <col> — совершенно нормальная вещь для производителя, который записывает ширину видимой колонки, а читатель, считающий любой ненулевой текст установленным, прячет эту колонку при загрузке. HotXLS проводит все эти атрибуты через одно преобразование xsd:boolean, которое обрезает окружающие пробелы, принимает цифры и слова и для всего остального возвращает собственный дефолт атрибута. Последняя ветка отражает решение о дизайне, которое стоит проговорить: значение вроде yes — признак битого файла, но отказ открывать книгу из-за одного испорченного флага видимости наказал бы пользователя за ошибку производителя, поэтому читатель держит фиксированный запасной вариант на каждый атрибут и идёт дальше
Читаем лист в стиле LibreOffice через HotXLS
Флаги уровня листа выходят наружу как обычные свойства TXLSXWorksheet: CustomHeight, ThickTop и ThickBottom приходят из <sheetFormatPr>, а ColHidden[], RowHidden[], RowThickTop[] и RowThickBottom[] принимают номера колонок и строк с единицы. Булева ячейка приезжает вариантом Boolean в TXLSXCell.Value — каким бы написанием ни был записан элемент <v>. Процедура ниже печатает всё это для первого листа, а список после неё даёт ожидаемый вывод для разметки в стиле Calc
uses
SysUtils, Variants, lxHandleX;
procedure DumpSheetBooleans(const FileName: string);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Flag: Variant;
Col, Row: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> 1 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Book.Sheets.Count = 0 then
raise Exception.Create('Workbook has no sheets');
Sheet := Book.Sheets[1]; // индексатор Sheets считается с единицы
Writeln('Default row height is custom: ', Sheet.CustomHeight);
Writeln('Rows default to thick top / bottom: ',
Sheet.ThickTop, ' / ', Sheet.ThickBottom);
for Col := 1 to 8 do
if Sheet.ColHidden[Col] then
Writeln('Column ', Col, ' is hidden');
for Row := 1 to 20 do
if Sheet.RowHidden[Row] or Sheet.RowThickTop[Row] or
Sheet.RowThickBottom[Row] then
Writeln('Row ', Row, ': hidden=', Sheet.RowHidden[Row],
' thickTop=', Sheet.RowThickTop[Row],
' thickBot=', Sheet.RowThickBottom[Row]);
Flag := Sheet.Cells[2, 1].Value;
if VarType(Flag) = varBoolean then
Writeln('A2 holds the boolean ', Boolean(Flag));
finally
Book.Free;
end;
end;
<sheetFormatPr defaultRowHeight="12.8" customHeight="true"/>:CustomHeightпечатает TRUE<col min="3" max="3" width="20" hidden="false"/>: колонка 3 не упоминается<row r="5" thickBot="true">: строка 5 сообщаетthickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 держит TRUE
Строчная подсказка — то место, где спотыкается сама схема. Одна идея, два написания: CT_SheetFormatPr зовёт свои атрибуты thickTop и thickBottom, а CT_Row использует thickTop и thickBot, так что читатель, ищущий thickBottom на строке, не найдёт ничего. HotXLS пишет каждую форму обратно на её собственный элемент, и подсказки переживают сохранение вместе с остальной частью пакета — как нетронутые части при круговом прогоне XLSX без потерь
Как диаграмме читать элемент без val?
Переключатель ChartML без атрибута val — включён. DrawingML Charts объявляет такие переключатели через CT_Boolean в dml-chart.xsd, чей val — необязательный xsd:boolean с дефолтом true, так что <c:plotVisOnly/> и <c:showHorzBorder/> означают то же, что записанный val="1". Этот дефолт обратен большинству атрибутов SpreadsheetML и ломает привычный трюк чтения XML диаграммы как текста: найти элемент по имени, затем искать вперёд ближайший val=". Когда элемент опускает val, поиск убегает за конец стартового тега и одалживает значение у какого-нибудь элемента дальше по части — возможно, у val="0" двумя сиблингами ниже. Когда не совпадает ничего, PosEx возвращает 0, и код, режущий по P + 5, оказывается читающим с пятого символа части — с буквы l из <?xml. Ни одна из этих неудач ничего не поднимает; свойство диаграммы просто расходится с файлом
Кавычки — вторая половина проблемы. XML 1.0 разрешает ограничивать значение атрибута как двойными, так и одинарными кавычками (продукция AttValue в §2.3), так что <c:majorTickMark val='out'/> так же легитимен, как форма с двойными, и поиск val=" проскочит его целиком. HotXLS хранит исходный ChartML, чтобы править диаграммы Excel, не теряя разметку, которую не моделирует, и переключатели, читаемые из этого текста, ищутся только внутри стартового тега самого элемента — с приёмом любого стиля кавычек и чтением отсутствующего val как дефолта CT_Boolean. Публичные свойства диаграмм отражают именно такое чтение
procedure DumpChartSwitches(Sheet: TXLSXWorksheet);
const
TickNames: array[TXLSXChartTickMark] of string =
('none', 'in', 'out', 'cross');
var
I: Integer;
Chart: TXLSXChart;
begin
for I := 0 to Sheet.Charts.Count - 1 do // Charts нуль-базированный
begin
Chart := Sheet.Charts[I];
Writeln(Chart.Name, ': plotVisOnly=', Chart.PlotVisibleOnly,
' dTable showHorzBorder=', Chart.ShowDataTableHorzBorder);
if Chart.ValueAxis <> nil then
Writeln(' value axis majorTickMark=',
TickNames[Chart.ValueAxis.MajorTickMark]);
if Chart.SeriesCount > 0 then
Writeln(' first series smooth=', Chart.Series[0].Smooth);
end;
end;
Ожидание для части диаграммы с <c:plotVisOnly/>, дальше по части — элементом с val="0", таблицей данных с <c:showHorzBorder/><c:showVertBorder val="0"/> и осью значений с <c:majorTickMark val='out'/>: PlotVisibleOnly и ShowDataTableHorzBorder оба TRUE, а отметка печатает out
Ограниченный читатель xsd:boolean для вашего собственного OOXML-кода
Если вы сами пост-обрабатываете пакет — скажем, кастомную часть, которую добавляет приложение, или беглый скан XML диаграмм на шаге сборки, — те же два правила в силе: конвертировать с полным лексическим пространством и никогда не давать поиску атрибута выйти за стартовый тег его элемента. Функции ниже делают и то и другое. ChartSwitch ждёт, что TagStart указывает на < элемента, и применяет дефолт CT_Boolean, когда val отсутствует
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: набивка легальна
Result := True;
if (S = '1') or (S = 'true') then
Value := True
else if (S = '0') or (S = 'false') then
Value := False
else
Result := False; // TRUE, yes, on: это не xsd:boolean
end;
function ReadXsdBoolean(const Literal: string; Default: Boolean): Boolean;
begin
if not TryParseXsdBoolean(Literal, Result) then
Result := Default;
end;
function AttrInStartTag(const Xml: string; TagStart: Integer;
const AttrName: string; out Value: string): Boolean;
var
TagEnd, P, Q: Integer;
Quote: Char;
begin
Result := False;
Value := '';
TagEnd := PosEx('>', Xml, TagStart);
if TagEnd = 0 then
Exit;
// имя должно начинать токен, а не заканчивать другое имя атрибута
P := PosEx(AttrName + '=', Xml, TagStart);
while (P > 0) and (P < TagEnd) and
not CharInSet(Xml[P - 1], [' ', #9, #10, #13]) do
P := PosEx(AttrName + '=', Xml, P + 1);
if (P = 0) or (P >= TagEnd) then
Exit;
Q := P + Length(AttrName) + 1;
Quote := Xml[Q];
if (Quote <> '"') and (Quote <> '''') then
Exit;
P := PosEx(Quote, Xml, Q + 1);
if (P = 0) or (P > TagEnd) then
Exit;
Value := Copy(Xml, Q + 1, P - Q - 1);
Result := True;
end;
function ChartSwitch(const Xml: string; TagStart: Integer): Boolean;
var
V: string;
begin
if not AttrInStartTag(Xml, TagStart, 'val', V) then
Exit(True); // CT_Boolean: отсутствие val означает true
Result := ReadXsdBoolean(V, True);
end;
Строгая ветка в TryParseXsdBoolean следует типу буква в букву, и здесь ваш читатель с HotXLS немного расходятся: преобразование HotXLS сравнивает слова без учёта регистра, так что TRUE читается как true. Мы скорее примем написание, запрещённое схемой, чем выключим флаг потому, что какой-то производитель написал его в верхнем регистре: ни один производитель не пишет TRUE, имея в виду false; валидатор, напротив, должен держать строгую ветку и сообщать о значении. Текстовый сканер тоже берёт два упрощения, безопасных для переключателей ChartML и неправильных для XML вообще. > легален внутри значения атрибута, поэтому остановка на самом раннем может обрезать тег, чьи значения его содержат, а XML разрешает пробелы вокруг =, которых поиск AttrName + '=' не видит. Значения переключателей ChartML — короткие токены без > внутри, а пропуск пробелов вокруг знака равенства — расширение на две строки, но для чего-либо сверх горстки известных переключателей XML-читатель, выдающий атрибуты по одному элементу за раз, снимает обе проблемы — именно такой HotXLS использует для частей листов
Когда книга Calc грузится с другими скрытыми колонками или высотами строк, чем её двойник из Excel, распакуйте обе и посмотрите, как написаны флаги; hidden="false" против hidden="0" обычно объясняет расхождение за минуту. Сопоставление обоих написаний, с набивкой или голых, одному набору свойств листов, ячеек и диаграмм — работа, которую компонент электронных таблиц HotXLS для Delphi делает при каждой загрузке