From 796741812df5a983bb75ce4c9f2e333e96856d83 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 13 Jul 2024 23:31:52 +0200 Subject: [PATCH 01/42] API for named styles --- SpreadCheetah/Spreadsheet.cs | 18 ++++++++++++++++++ SpreadCheetah/Styling/StyleNameVisibility.cs | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 SpreadCheetah/Styling/StyleNameVisibility.cs diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index 7216e2a3..b620ab65 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -31,6 +31,7 @@ public sealed class Spreadsheet : IDisposable, IAsyncDisposable private readonly NumberFormat? _defaultDateTimeFormat; private DefaultStyling? _defaultStyling; private Dictionary? _styles; + private Dictionary? _namedStyles; private FileCounter? _fileCounter; private Worksheet? _worksheet; private bool _disposed; @@ -394,6 +395,22 @@ public StyleId AddStyle(Style style) return AddStyle(ImmutableStyle.From(style)); } + public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleNameVisibility = null) + { + ArgumentNullException.ThrowIfNull(style); + ArgumentNullException.ThrowIfNull(name); + + // TODO: Validate name length + // TODO: Validate uniqueness of name (does case sensitivity matter?) + + var styleId = AddStyle(style); + + _namedStyles ??= []; + _namedStyles[name] = (styleId, styleNameVisibility); + + return styleId; + } + private void AddDefaultStyle() { var defaultFont = new ImmutableFont(null, false, false, false, Font.DefaultSize, null); @@ -615,6 +632,7 @@ private async ValueTask FinishInternalAsync(CancellationToken token) await WorkbookRelsXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, hasStyles, token).ConfigureAwait(false); await WorkbookXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, token).ConfigureAwait(false); + // TODO: Pass named styles if (_styles is not null) await StylesXml.WriteAsync(_archive, _compressionLevel, _buffer, _styles, token).ConfigureAwait(false); diff --git a/SpreadCheetah/Styling/StyleNameVisibility.cs b/SpreadCheetah/Styling/StyleNameVisibility.cs new file mode 100644 index 00000000..1739595d --- /dev/null +++ b/SpreadCheetah/Styling/StyleNameVisibility.cs @@ -0,0 +1,7 @@ +namespace SpreadCheetah.Styling; + +public enum StyleNameVisibility +{ + Visible, + Hidden +} From b5d7f1a849e0290dc530bde4a88c04f908dfbe92 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 26 Jul 2024 09:59:53 +0200 Subject: [PATCH 02/42] Implement GetStyleId --- SpreadCheetah/Helpers/ThrowHelper.cs | 3 +++ SpreadCheetah/Spreadsheet.cs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/SpreadCheetah/Helpers/ThrowHelper.cs b/SpreadCheetah/Helpers/ThrowHelper.cs index 2779a48c..c68dc3f9 100644 --- a/SpreadCheetah/Helpers/ThrowHelper.cs +++ b/SpreadCheetah/Helpers/ThrowHelper.cs @@ -73,6 +73,9 @@ internal static class ThrowHelper [DoesNotReturn] public static void StreamContentNotSupportedImageType(string? paramName) => throw new ArgumentException("The stream content is not a supported image type. Currently only PNG images are supported.", nameof(paramName)); + [DoesNotReturn] + public static void StyleNotFound(string name) => throw new SpreadCheetahException($"Style with name '{name}' was not found. Make sure the style is first added to the spreadsheet by calling {nameof(Spreadsheet.AddStyle)} with a name argument."); + [DoesNotReturn] public static void ValueIsNegative(string? paramName, T value) => throw new ArgumentOutOfRangeException(paramName, value, "The value can not be negative."); diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index b620ab65..97429fb1 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -447,6 +447,22 @@ private StyleId AddStyle(in ImmutableStyle style) return new StyleId(newId, newDateTimeId); } + /// + /// Get the from a named style. + /// The named style must have previously been added to the spreadsheet with . + /// If the named style is not found, a is thrown. + /// + public StyleId GetStyleId(string name) + { + ArgumentNullException.ThrowIfNull(name); + + if (_namedStyles is { } namedStyles && namedStyles.TryGetValue(name, out var value)) + return value.Item1; + + ThrowHelper.StyleNotFound(name); + return null; // Unreachable + } + /// /// Adds data validation for a cell or a range of cells. The reference must be in the A1 reference style. Some examples: /// From 0b894c760f7cb399ef622e7ccb7321c9939880f1 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 26 Jul 2024 19:05:50 +0200 Subject: [PATCH 03/42] Validate name when adding a named style --- SpreadCheetah/Helpers/ThrowHelper.cs | 8 +++++++- SpreadCheetah/Spreadsheet.cs | 15 +++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/SpreadCheetah/Helpers/ThrowHelper.cs b/SpreadCheetah/Helpers/ThrowHelper.cs index c68dc3f9..e7f195ec 100644 --- a/SpreadCheetah/Helpers/ThrowHelper.cs +++ b/SpreadCheetah/Helpers/ThrowHelper.cs @@ -74,7 +74,13 @@ internal static class ThrowHelper public static void StreamContentNotSupportedImageType(string? paramName) => throw new ArgumentException("The stream content is not a supported image type. Currently only PNG images are supported.", nameof(paramName)); [DoesNotReturn] - public static void StyleNotFound(string name) => throw new SpreadCheetahException($"Style with name '{name}' was not found. Make sure the style is first added to the spreadsheet by calling {nameof(Spreadsheet.AddStyle)} with a name argument."); + public static void StyleNameAlreadyExists(string? paramName) => throw new ArgumentException("A style with the given name already exists.", paramName); + + [DoesNotReturn] + public static void StyleNameNotFound(string name) => throw new SpreadCheetahException($"Style with name '{name}' was not found. Make sure the style is first added to the spreadsheet by calling {nameof(Spreadsheet.AddStyle)} with a name argument."); + + [DoesNotReturn] + public static void StyleNameTooLong(string? paramName) => throw new ArgumentException("The name can not be more than 255 characters.", paramName); [DoesNotReturn] public static void ValueIsNegative(string? paramName, T value) => throw new ArgumentOutOfRangeException(paramName, value, "The value can not be negative."); diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index 97429fb1..cb4727a9 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -400,13 +400,16 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName ArgumentNullException.ThrowIfNull(style); ArgumentNullException.ThrowIfNull(name); - // TODO: Validate name length - // TODO: Validate uniqueness of name (does case sensitivity matter?) + if (name.Length > 255) + ThrowHelper.StyleNameTooLong(nameof(name)); - var styleId = AddStyle(style); + // TODO: Validate uniqueness of name (does case sensitivity matter? does whitespace matter?) + var namedStyles = _namedStyles ??= []; + if (namedStyles.ContainsKey(name)) + ThrowHelper.StyleNameAlreadyExists(nameof(name)); - _namedStyles ??= []; - _namedStyles[name] = (styleId, styleNameVisibility); + var styleId = AddStyle(style); + namedStyles[name] = (styleId, styleNameVisibility); return styleId; } @@ -459,7 +462,7 @@ public StyleId GetStyleId(string name) if (_namedStyles is { } namedStyles && namedStyles.TryGetValue(name, out var value)) return value.Item1; - ThrowHelper.StyleNotFound(name); + ThrowHelper.StyleNameNotFound(name); return null; // Unreachable } From ea4b0242c09da6ab75c24cddb90dbc58abc5c647 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 26 Jul 2024 19:09:09 +0200 Subject: [PATCH 04/42] Simplify comparing with default alignment --- SpreadCheetah/MetadataXml/StylesXml.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SpreadCheetah/MetadataXml/StylesXml.cs b/SpreadCheetah/MetadataXml/StylesXml.cs index e96d3e84..e060bf5d 100644 --- a/SpreadCheetah/MetadataXml/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/StylesXml.cs @@ -198,8 +198,7 @@ private bool TryWriteStyles(Span bytes, ref int bytesWritten) if (!" xfId=\"0\""u8.TryCopyTo(span, ref written)) return false; - var defaultAlignment = new ImmutableAlignment(); - if (style.Alignment == defaultAlignment) + if (style.Alignment == default) { if (!"/>"u8.TryCopyTo(span, ref written)) return false; } From 4c69d2e829436c34fc1cb709a71cfe3fa950ab92 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 26 Jul 2024 19:17:41 +0200 Subject: [PATCH 05/42] Remove unnecessary passing of SpreadsheetBuffer as argument in WorksheetStartXml --- SpreadCheetah/MetadataXml/WorksheetStartXml.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SpreadCheetah/MetadataXml/WorksheetStartXml.cs b/SpreadCheetah/MetadataXml/WorksheetStartXml.cs index d5da893f..ad8e7895 100644 --- a/SpreadCheetah/MetadataXml/WorksheetStartXml.cs +++ b/SpreadCheetah/MetadataXml/WorksheetStartXml.cs @@ -49,8 +49,8 @@ public bool MoveNext() Current = _next switch { Element.Header => _buffer.TryWrite(Header), - Element.SheetViews => TryWriteSheetViews(_buffer), - Element.Columns => TryWriteColumns(_buffer), + Element.SheetViews => TryWriteSheetViews(), + Element.Columns => TryWriteColumns(), _ => _buffer.TryWrite(SheetDataBegin) }; @@ -60,13 +60,13 @@ public bool MoveNext() return _next < Element.Done; } - private readonly bool TryWriteSheetViews(SpreadsheetBuffer buffer) + private readonly bool TryWriteSheetViews() { var options = _options; if (options?.FrozenColumns is null && options?.FrozenRows is null) return true; - var span = buffer.GetSpan(); + var span = _buffer.GetSpan(); var written = 0; if (!" bytes, ref int bytesWritten) return "\" "u8.TryCopyTo(bytes, ref bytesWritten); } - private bool TryWriteColumns(SpreadsheetBuffer buffer) + private bool TryWriteColumns() { if (_columns is not { } columns) return true; @@ -140,7 +140,7 @@ private bool TryWriteColumns(SpreadsheetBuffer buffer) if (options.Width is null && !options.Hidden) continue; - var span = buffer.GetSpan(); + var span = _buffer.GetSpan(); var written = 0; if (!anyColumnWritten) @@ -166,13 +166,13 @@ private bool TryWriteColumns(SpreadsheetBuffer buffer) _anyColumnWritten = anyColumnWritten; - buffer.Advance(written); + _buffer.Advance(written); } if (!anyColumnWritten) return true; - return buffer.TryWrite(""u8); + return _buffer.TryWrite(""u8); } private enum Element From 33280c3b5eb8dd0a74026a21805754c3b57f6f76 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 00:21:17 +0200 Subject: [PATCH 06/42] Enumerators for styles XML --- SpreadCheetah/MetadataXml/StyleBordersXml.cs | 56 ++++++------ SpreadCheetah/MetadataXml/StyleFillsXml.cs | 56 ++++++------ SpreadCheetah/MetadataXml/StyleFontsXml.cs | 51 ++++++----- .../MetadataXml/StyleNumberFormatsXml.cs | 57 ++++++------ SpreadCheetah/MetadataXml/StylesXml.cs | 86 +++++++++++-------- 5 files changed, 170 insertions(+), 136 deletions(-) diff --git a/SpreadCheetah/MetadataXml/StyleBordersXml.cs b/SpreadCheetah/MetadataXml/StyleBordersXml.cs index 331fd76a..e3acbae2 100644 --- a/SpreadCheetah/MetadataXml/StyleBordersXml.cs +++ b/SpreadCheetah/MetadataXml/StyleBordersXml.cs @@ -5,41 +5,46 @@ namespace SpreadCheetah.MetadataXml; -internal struct StyleBordersXml +internal struct StyleBordersXml(List borders, SpreadsheetBuffer buffer) { - private readonly List _borders; private Element _next; private int _nextIndex; - public StyleBordersXml(List borders) + public bool TryWrite() { - _borders = borders; - } - - public bool TryWrite(Span bytes, ref int bytesWritten) - { - if (_next == Element.Header && !Advance(TryWriteHeader(bytes, ref bytesWritten))) return false; - if (_next == Element.Borders && !Advance(TryWriteBorders(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(""u8.TryCopyTo(bytes, ref bytesWritten))) return false; + while (MoveNext()) + { + if (!Current) + return false; + } return true; } - private bool Advance(bool success) + public bool Current { get; private set; } + + public bool MoveNext() { - if (success) + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.Borders => TryWriteBorders(), + _ => buffer.TryWrite(""u8) + }; + + if (Current) ++_next; - return success; + return _next < Element.Done; } - private readonly bool TryWriteHeader(Span bytes, ref int bytesWritten) + private readonly bool TryWriteHeader() { - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; const int defaultCount = 1; - var totalCount = _borders.Count + defaultCount - 1; + var totalCount = borders.Count + defaultCount - 1; if (!" bytes, ref int bytesWritten) ""u8; if (!defaultBorder.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); return true; } - private bool TryWriteBorders(Span bytes, ref int bytesWritten) + private bool TryWriteBorders() { - var defaultBorder = new ImmutableBorder(); - var borders = _borders; + var bordersLocal = borders; - for (; _nextIndex < borders.Count; ++_nextIndex) + for (; _nextIndex < bordersLocal.Count; ++_nextIndex) { - var border = borders[_nextIndex]; - if (border.Equals(defaultBorder)) continue; + var border = bordersLocal[_nextIndex]; + if (border.Equals(default)) continue; - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; var diag = border.Diagonal; @@ -80,7 +84,7 @@ private bool TryWriteBorders(Span bytes, ref int bytesWritten) if (!""u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); } return true; diff --git a/SpreadCheetah/MetadataXml/StyleFillsXml.cs b/SpreadCheetah/MetadataXml/StyleFillsXml.cs index 731b69d9..292d26cf 100644 --- a/SpreadCheetah/MetadataXml/StyleFillsXml.cs +++ b/SpreadCheetah/MetadataXml/StyleFillsXml.cs @@ -3,41 +3,46 @@ namespace SpreadCheetah.MetadataXml; -internal struct StyleFillsXml +internal struct StyleFillsXml(List fills, SpreadsheetBuffer buffer) { - private readonly List _fills; private Element _next; private int _nextIndex; - public StyleFillsXml(List fills) + public bool TryWrite() { - _fills = fills; - } - - public bool TryWrite(Span bytes, ref int bytesWritten) - { - if (_next == Element.Header && !Advance(TryWriteHeader(bytes, ref bytesWritten))) return false; - if (_next == Element.Fills && !Advance(TryWriteFills(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(""u8.TryCopyTo(bytes, ref bytesWritten))) return false; + while (MoveNext()) + { + if (!Current) + return false; + } return true; } - private bool Advance(bool success) + public bool Current { get; private set; } + + public bool MoveNext() { - if (success) + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.Fills => TryWriteFills(), + _ => buffer.TryWrite(""u8) + }; + + if (Current) ++_next; - return success; + return _next < Element.Done; } - private readonly bool TryWriteHeader(Span bytes, ref int bytesWritten) + private readonly bool TryWriteHeader() { - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; const int defaultCount = 2; - var totalCount = _fills.Count + defaultCount - 1; + var totalCount = fills.Count + defaultCount - 1; if (!" bytes, ref int bytesWritten) """"""u8; if (!defaultFills.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); return true; } - private bool TryWriteFills(Span bytes, ref int bytesWritten) + private bool TryWriteFills() { - var defaultFill = new ImmutableFill(); - var fills = _fills; + var fillsLocal = fills; - for (; _nextIndex < fills.Count; ++_nextIndex) + for (; _nextIndex < fillsLocal.Count; ++_nextIndex) { - var fill = fills[_nextIndex]; - if (fill.Equals(defaultFill)) continue; + var fill = fillsLocal[_nextIndex]; + if (fill.Equals(default)) continue; if (fill.Color is not { } color) continue; - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; if (!""u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); } return true; diff --git a/SpreadCheetah/MetadataXml/StyleFontsXml.cs b/SpreadCheetah/MetadataXml/StyleFontsXml.cs index 91a61fcc..f82d148c 100644 --- a/SpreadCheetah/MetadataXml/StyleFontsXml.cs +++ b/SpreadCheetah/MetadataXml/StyleFontsXml.cs @@ -4,41 +4,46 @@ namespace SpreadCheetah.MetadataXml; -internal struct StyleFontsXml +internal struct StyleFontsXml(List fonts, SpreadsheetBuffer buffer) { - private readonly List _fonts; private Element _next; private int _nextIndex; - public StyleFontsXml(List fonts) + public bool TryWrite() { - _fonts = fonts; - } - - public bool TryWrite(Span bytes, ref int bytesWritten) - { - if (_next == Element.Header && !Advance(TryWriteHeader(bytes, ref bytesWritten))) return false; - if (_next == Element.Fonts && !Advance(TryWriteFonts(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(""u8.TryCopyTo(bytes, ref bytesWritten))) return false; + while (MoveNext()) + { + if (!Current) + return false; + } return true; } - private bool Advance(bool success) + public bool Current { get; private set; } + + public bool MoveNext() { - if (success) + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.Fonts => TryWriteFonts(), + _ => buffer.TryWrite(""u8) + }; + + if (Current) ++_next; - return success; + return _next < Element.Done; } - private readonly bool TryWriteHeader(Span bytes, ref int bytesWritten) + private readonly bool TryWriteHeader() { - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; const int defaultCount = 1; - var totalCount = _fonts.Count + defaultCount - 1; + var totalCount = fonts.Count + defaultCount - 1; if (!" bytes, ref int bytesWritten) """"""u8; if (!defaultFont.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); return true; } - private bool TryWriteFonts(Span bytes, ref int bytesWritten) + private bool TryWriteFonts() { var defaultFont = new ImmutableFont { Size = Font.DefaultSize }; - var fonts = _fonts; + var fontsLocal = fonts; - for (; _nextIndex < fonts.Count; ++_nextIndex) + for (; _nextIndex < fontsLocal.Count; ++_nextIndex) { var font = fonts[_nextIndex]; if (font.Equals(defaultFont)) continue; - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; if (!""u8.TryCopyTo(span, ref written)) return false; @@ -85,7 +90,7 @@ private bool TryWriteFonts(Span bytes, ref int bytesWritten) if (!SpanHelper.TryWrite(fontName, span, ref written)) return false; if (!"\"/>"u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); } return true; diff --git a/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs b/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs index 6a257cdf..2fa11644 100644 --- a/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs +++ b/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs @@ -2,59 +2,66 @@ namespace SpreadCheetah.MetadataXml; -internal struct StyleNumberFormatsXml +internal struct StyleNumberFormatsXml( + List>? customNumberFormats, + SpreadsheetBuffer buffer) { - private readonly List>? _customNumberFormats; private Element _next; private int _nextIndex; - public StyleNumberFormatsXml(List>? customNumberFormats) + public bool TryWrite() { - _customNumberFormats = customNumberFormats; - } - - public bool TryWrite(Span bytes, ref int bytesWritten) - { - if (_next == Element.Header && !Advance(TryWriteHeader(bytes, ref bytesWritten))) return false; - if (_next == Element.NumberFormats && !Advance(TryWriteNumberFormats(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(TryWriteFooter(bytes, ref bytesWritten))) return false; + while (MoveNext()) + { + if (!Current) + return false; + } return true; } - private bool Advance(bool success) + public bool Current { get; private set; } + + public bool MoveNext() { - if (success) + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.NumberFormats => TryWriteNumberFormats(), + _ => TryWriteFooter() + }; + + if (Current) ++_next; - return success; + return _next < Element.Done; } - private readonly bool TryWriteHeader(Span bytes, ref int bytesWritten) + private readonly bool TryWriteHeader() { - if (_customNumberFormats is not { } formats) - return """"""u8.TryCopyTo(bytes, ref bytesWritten); + if (customNumberFormats is not { } formats) + return buffer.TryWrite(""""""u8); - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; if (!""u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); return true; } - private bool TryWriteNumberFormats(Span bytes, ref int bytesWritten) + private bool TryWriteNumberFormats() { - if (_customNumberFormats is not { } formats) + if (customNumberFormats is not { } formats) return true; for (; _nextIndex < formats.Count; ++_nextIndex) { var format = formats[_nextIndex]; - var span = bytes.Slice(bytesWritten); + var span = buffer.GetSpan(); var written = 0; if (!" bytes, ref int bytesWritten) if (!SpanHelper.TryWrite(numberFormat, span, ref written)) return false; if (!"\"/>"u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + buffer.Advance(written); } return true; } - private readonly bool TryWriteFooter(Span bytes, ref int bytesWritten) - => _customNumberFormats is null || ""u8.TryCopyTo(bytes, ref bytesWritten); + private readonly bool TryWriteFooter() + => customNumberFormats is null || buffer.TryWrite(""u8); private enum Element { diff --git a/SpreadCheetah/MetadataXml/StylesXml.cs b/SpreadCheetah/MetadataXml/StylesXml.cs index e060bf5d..f984e055 100644 --- a/SpreadCheetah/MetadataXml/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/StylesXml.cs @@ -5,9 +5,9 @@ namespace SpreadCheetah.MetadataXml; -internal struct StylesXml : IXmlWriter +internal struct StylesXml { - public static ValueTask WriteAsync( + public static async ValueTask WriteAsync( ZipArchive archive, CompressionLevel compressionLevel, SpreadsheetBuffer buffer, @@ -15,10 +15,23 @@ public static ValueTask WriteAsync( CancellationToken token) { var entry = archive.CreateEntry("xl/styles.xml", compressionLevel); - var writer = new StylesXml([.. styles.Keys]); -#pragma warning disable EPS06 // Hidden struct copy operation - return writer.WriteAsync(entry, buffer, token); -#pragma warning restore EPS06 // Hidden struct copy operation + var stream = entry.Open(); +#if NETSTANDARD2_0 + using (stream) +#else + await using (stream.ConfigureAwait(false)) +#endif + { + var writer = new StylesXml([.. styles.Keys], buffer); + + foreach (var success in writer) + { + if (!success) + await buffer.FlushToStreamAsync(stream, token).ConfigureAwait(false); + } + + await buffer.FlushToStreamAsync(stream, token).ConfigureAwait(false); + } } private static ReadOnlySpan Header => @@ -38,6 +51,7 @@ public static ValueTask WriteAsync( private readonly Dictionary _borders; private readonly Dictionary _fills; private readonly Dictionary _fonts; + private readonly SpreadsheetBuffer _buffer; private StyleNumberFormatsXml _numberFormatsXml; private StyleBordersXml _bordersXml; private StyleFillsXml _fillsXml; @@ -45,19 +59,23 @@ public static ValueTask WriteAsync( private Element _next; private int _nextIndex; - private StylesXml(List styles) + private StylesXml(List styles, SpreadsheetBuffer buffer) { _customNumberFormats = CreateCustomNumberFormatDictionary(styles); _borders = CreateBorderDictionary(styles); _fills = CreateFillDictionary(styles); _fonts = CreateFontDictionary(styles); - _numberFormatsXml = new StyleNumberFormatsXml(_customNumberFormats is { } formats ? [.. formats] : null); - _bordersXml = new StyleBordersXml([.. _borders.Keys]); - _fillsXml = new StyleFillsXml([.. _fills.Keys]); - _fontsXml = new StyleFontsXml([.. _fonts.Keys]); + _buffer = buffer; + _numberFormatsXml = new StyleNumberFormatsXml(_customNumberFormats is { } formats ? [.. formats] : null, buffer); + _bordersXml = new StyleBordersXml([.. _borders.Keys], buffer); + _fillsXml = new StyleFillsXml([.. _fills.Keys], buffer); + _fontsXml = new StyleFontsXml([.. _fonts.Keys], buffer); _styles = styles; } + public readonly StylesXml GetEnumerator() => this; + public bool Current { get; private set; } + private static Dictionary? CreateCustomNumberFormatDictionary(List styles) { Dictionary? dictionary = null; @@ -128,33 +146,29 @@ private static Dictionary CreateFontDictionary(List bytes, out int bytesWritten) - { - bytesWritten = 0; - - if (_next == Element.Header && !Advance(Header.TryCopyTo(bytes, ref bytesWritten))) return false; - if (_next == Element.NumberFormats && !Advance(_numberFormatsXml.TryWrite(bytes, ref bytesWritten))) return false; - if (_next == Element.Fonts && !Advance(_fontsXml.TryWrite(bytes, ref bytesWritten))) return false; - if (_next == Element.Fills && !Advance(_fillsXml.TryWrite(bytes, ref bytesWritten))) return false; - if (_next == Element.Borders && !Advance(_bordersXml.TryWrite(bytes, ref bytesWritten))) return false; - if (_next == Element.CellXfsStart && !Advance(TryWriteCellXfsStart(bytes, ref bytesWritten))) return false; - if (_next == Element.Styles && !Advance(TryWriteStyles(bytes, ref bytesWritten))) return false; - if (_next == Element.Footer && !Advance(Footer.TryCopyTo(bytes, ref bytesWritten))) return false; - - return true; - } - - private bool Advance(bool success) + public bool MoveNext() { - if (success) + Current = _next switch + { + Element.Header => _buffer.TryWrite(Header), + Element.NumberFormats => _numberFormatsXml.TryWrite(), + Element.Fonts => _fontsXml.TryWrite(), + Element.Fills => _fillsXml.TryWrite(), + Element.Borders => _bordersXml.TryWrite(), + Element.CellXfsStart => TryWriteCellXfsStart(), + Element.Styles => TryWriteStyles(), + _ => _buffer.TryWrite(Footer), + }; + + if (Current) ++_next; - return success; + return _next < Element.Done; } - private readonly bool TryWriteCellXfsStart(Span bytes, ref int bytesWritten) + private readonly bool TryWriteCellXfsStart() { - var span = bytes.Slice(bytesWritten); + var span = _buffer.GetSpan(); var written = 0; ReadOnlySpan xml = @@ -165,18 +179,18 @@ private readonly bool TryWriteCellXfsStart(Span bytes, ref int bytesWritte if (!SpanHelper.TryWrite(_styles.Count, span, ref written)) return false; if (!"\">"u8.TryCopyTo(span, ref written)) return false; - bytesWritten += written; + _buffer.Advance(written); return true; } - private bool TryWriteStyles(Span bytes, ref int bytesWritten) + private bool TryWriteStyles() { var styles = _styles; for (; _nextIndex < styles.Count; ++_nextIndex) { var style = _styles[_nextIndex]; - var span = bytes.Slice(bytesWritten); + var span = _buffer.GetSpan(); var written = 0; var numberFormatId = GetNumberFormatId(style.Format, _customNumberFormats); @@ -209,7 +223,7 @@ private bool TryWriteStyles(Span bytes, ref int bytesWritten) if (!""u8.TryCopyTo(span, ref written)) return false; } - bytesWritten += written; + _buffer.Advance(written); } return true; From 46d50d59c5dc7308b5244fddd5947fc20add6914 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 10:55:16 +0200 Subject: [PATCH 07/42] Fix a potential issue with the order of styles --- SpreadCheetah/MetadataXml/StylesXml.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SpreadCheetah/MetadataXml/StylesXml.cs b/SpreadCheetah/MetadataXml/StylesXml.cs index f984e055..10fa61c2 100644 --- a/SpreadCheetah/MetadataXml/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/StylesXml.cs @@ -22,7 +22,10 @@ public static async ValueTask WriteAsync( await using (stream.ConfigureAwait(false)) #endif { - var writer = new StylesXml([.. styles.Keys], buffer); + // The order of Dictionary.Keys is not guaranteed, so we make sure the styles are sorted by the StyleId here. + var orderedStyles = styles.OrderBy(x => x.Value).Select(x => x.Key).ToList(); + + var writer = new StylesXml(orderedStyles, buffer); foreach (var success in writer) { From affb3cb5874e2e58d5034509e2008e06404e90ca Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 17:47:13 +0200 Subject: [PATCH 08/42] Update public API snapshots --- ...ublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt | 7 +++++++ ...ublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt | 7 +++++++ ...ublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt | 7 +++++++ .../PublicApiTests.PublicApi_Generate.Net4_7.verified.txt | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt index 3d962e2c..baa3f57a 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt @@ -116,10 +116,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -400,6 +402,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt index 3ec073e4..f4f1175d 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt @@ -116,10 +116,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -400,6 +402,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt index b7b215bb..f7321583 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt @@ -116,10 +116,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -400,6 +402,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt index 5f6676c6..ac3de460 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt @@ -115,10 +115,12 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask FinishAsync(System.Threading.CancellationToken token = default) { } + public SpreadCheetah.Styling.StyleId GetStyleId(string name) { } public void MergeCells(string cellRange) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.Worksheets.WorksheetOptions? options = null, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask StartWorksheetAsync(string name, SpreadCheetah.SourceGeneration.WorksheetRowTypeInfo typeInfo, System.Threading.CancellationToken token = default) { } @@ -399,6 +401,11 @@ namespace SpreadCheetah.Styling { public int Id { get; } } + public enum StyleNameVisibility + { + Visible = 0, + Hidden = 1, + } public enum VerticalAlignment { Bottom = 0, From fbd5da42a2d82973346948e73e0d90ad37e579b3 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 20:29:18 +0200 Subject: [PATCH 09/42] Writer for cellStyles in styles.xml --- .../MetadataXml/StyleCellStylesXml.cs | 99 +++++++++++++++++++ SpreadCheetah/MetadataXml/StylesXml.cs | 23 +++-- SpreadCheetah/Spreadsheet.cs | 4 +- 3 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 SpreadCheetah/MetadataXml/StyleCellStylesXml.cs diff --git a/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs b/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs new file mode 100644 index 00000000..548c4489 --- /dev/null +++ b/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs @@ -0,0 +1,99 @@ +using SpreadCheetah.Helpers; +using SpreadCheetah.Styling; + +namespace SpreadCheetah.MetadataXml; + +internal struct StyleCellStylesXml( + List<(string, StyleId, StyleNameVisibility)>? namedStyles, + SpreadsheetBuffer buffer) +{ + private string? _currentXmlEncodedName; + private int _currentXmlEncodedNameIndex; + private Element _next; + private int _nextIndex; + +#pragma warning disable EPS12 // A struct member can be made readonly + public bool TryWrite() +#pragma warning restore EPS12 // A struct member can be made readonly + { + while (MoveNext()) + { + if (!Current) + return false; + } + + return true; + } + + public bool Current { get; private set; } + + public bool MoveNext() + { + Current = _next switch + { + Element.Header => TryWriteHeader(), + Element.CellStyles => TryWriteCellStyles(), + _ => buffer.TryWrite(""u8) + }; + + if (Current) + ++_next; + + return _next < Element.Done; + } + + private readonly bool TryWriteHeader() + { + var count = (namedStyles?.Count ?? 0) + 1; + return buffer.TryWrite( + $"{""u8}"); + } + + private bool TryWriteCellStyles() + { + if (namedStyles is not { } namedStylesLocal) + return true; + + for (; _nextIndex < namedStylesLocal.Count; ++_nextIndex) + { + var (name, _, _) = namedStylesLocal[_nextIndex]; + var span = buffer.GetSpan(); + var written = 0; + + if (_currentXmlEncodedName is null) + { + if (!""u8)) return false; + + _currentXmlEncodedName = null; + _currentXmlEncodedNameIndex = 0; + } + + return true; + } + + private enum Element + { + Header, + CellStyles, + Footer, + Done + } +} diff --git a/SpreadCheetah/MetadataXml/StylesXml.cs b/SpreadCheetah/MetadataXml/StylesXml.cs index 10fa61c2..dd7e2ac8 100644 --- a/SpreadCheetah/MetadataXml/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/StylesXml.cs @@ -12,6 +12,7 @@ public static async ValueTask WriteAsync( CompressionLevel compressionLevel, SpreadsheetBuffer buffer, Dictionary styles, + Dictionary? namedStyles, CancellationToken token) { var entry = archive.CreateEntry("xl/styles.xml", compressionLevel); @@ -25,7 +26,12 @@ public static async ValueTask WriteAsync( // The order of Dictionary.Keys is not guaranteed, so we make sure the styles are sorted by the StyleId here. var orderedStyles = styles.OrderBy(x => x.Value).Select(x => x.Key).ToList(); - var writer = new StylesXml(orderedStyles, buffer); + var filteredNamedStyles = namedStyles? + .Where(x => x.Value.Item2 is not null) + .Select(x => (x.Key, x.Value.Item1, x.Value.Item2.GetValueOrDefault())) + .ToList(); + + var writer = new StylesXml(orderedStyles, filteredNamedStyles, buffer); foreach (var success in writer) { @@ -42,10 +48,6 @@ public static async ValueTask WriteAsync( """"""u8; private static ReadOnlySpan Footer => - """"""u8 + - """"""u8 + - """"""u8 + - """"""u8 + """"""u8 + """"""u8; @@ -59,10 +61,14 @@ public static async ValueTask WriteAsync( private StyleBordersXml _bordersXml; private StyleFillsXml _fillsXml; private StyleFontsXml _fontsXml; + private StyleCellStylesXml _cellStylesXml; private Element _next; private int _nextIndex; - private StylesXml(List styles, SpreadsheetBuffer buffer) + private StylesXml( + List styles, + List<(string, StyleId, StyleNameVisibility)>? namedStyles, + SpreadsheetBuffer buffer) { _customNumberFormats = CreateCustomNumberFormatDictionary(styles); _borders = CreateBorderDictionary(styles); @@ -73,6 +79,7 @@ private StylesXml(List styles, SpreadsheetBuffer buffer) _bordersXml = new StyleBordersXml([.. _borders.Keys], buffer); _fillsXml = new StyleFillsXml([.. _fills.Keys], buffer); _fontsXml = new StyleFontsXml([.. _fonts.Keys], buffer); + _cellStylesXml = new StyleCellStylesXml(namedStyles, buffer); _styles = styles; } @@ -160,6 +167,8 @@ public bool MoveNext() Element.Borders => _bordersXml.TryWrite(), Element.CellXfsStart => TryWriteCellXfsStart(), Element.Styles => TryWriteStyles(), + Element.CellXfsEnd => _buffer.TryWrite(""u8), + Element.CellStyles => _cellStylesXml.TryWrite(), _ => _buffer.TryWrite(Footer), }; @@ -305,6 +314,8 @@ private enum Element Borders, CellXfsStart, Styles, + CellXfsEnd, + CellStyles, Footer, Done } diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index cb4727a9..a41aa761 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -404,6 +404,7 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName ThrowHelper.StyleNameTooLong(nameof(name)); // TODO: Validate uniqueness of name (does case sensitivity matter? does whitespace matter?) + // TODO: Can the name "Normal" be used? var namedStyles = _namedStyles ??= []; if (namedStyles.ContainsKey(name)) ThrowHelper.StyleNameAlreadyExists(nameof(name)); @@ -651,9 +652,8 @@ private async ValueTask FinishInternalAsync(CancellationToken token) await WorkbookRelsXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, hasStyles, token).ConfigureAwait(false); await WorkbookXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, token).ConfigureAwait(false); - // TODO: Pass named styles if (_styles is not null) - await StylesXml.WriteAsync(_archive, _compressionLevel, _buffer, _styles, token).ConfigureAwait(false); + await StylesXml.WriteAsync(_archive, _compressionLevel, _buffer, _styles, _namedStyles, token).ConfigureAwait(false); _finished = true; From 012da35170bdaee8f9ce4e9969177f93f4668839 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 20:41:24 +0200 Subject: [PATCH 10/42] Add TODO comments for styles.xml --- SpreadCheetah/MetadataXml/StyleCellStylesXml.cs | 1 + SpreadCheetah/MetadataXml/StylesXml.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs b/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs index 548c4489..1e91d03a 100644 --- a/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs +++ b/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs @@ -62,6 +62,7 @@ private bool TryWriteCellStyles() var span = buffer.GetSpan(); var written = 0; + // TODO: Handle "hidden" if (_currentXmlEncodedName is null) { if (!" Date: Sat, 27 Jul 2024 20:43:43 +0200 Subject: [PATCH 11/42] Move styles.xml writers to subfolder --- SpreadCheetah/MetadataXml/{ => Styles}/StyleBordersXml.cs | 2 +- SpreadCheetah/MetadataXml/{ => Styles}/StyleCellStylesXml.cs | 2 +- SpreadCheetah/MetadataXml/{ => Styles}/StyleFillsXml.cs | 2 +- SpreadCheetah/MetadataXml/{ => Styles}/StyleFontsXml.cs | 2 +- SpreadCheetah/MetadataXml/{ => Styles}/StyleNumberFormatsXml.cs | 2 +- SpreadCheetah/MetadataXml/{ => Styles}/StylesXml.cs | 2 +- SpreadCheetah/Spreadsheet.cs | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) rename SpreadCheetah/MetadataXml/{ => Styles}/StyleBordersXml.cs (99%) rename SpreadCheetah/MetadataXml/{ => Styles}/StyleCellStylesXml.cs (98%) rename SpreadCheetah/MetadataXml/{ => Styles}/StyleFillsXml.cs (98%) rename SpreadCheetah/MetadataXml/{ => Styles}/StyleFontsXml.cs (98%) rename SpreadCheetah/MetadataXml/{ => Styles}/StyleNumberFormatsXml.cs (98%) rename SpreadCheetah/MetadataXml/{ => Styles}/StylesXml.cs (99%) diff --git a/SpreadCheetah/MetadataXml/StyleBordersXml.cs b/SpreadCheetah/MetadataXml/Styles/StyleBordersXml.cs similarity index 99% rename from SpreadCheetah/MetadataXml/StyleBordersXml.cs rename to SpreadCheetah/MetadataXml/Styles/StyleBordersXml.cs index e3acbae2..53f4eaa1 100644 --- a/SpreadCheetah/MetadataXml/StyleBordersXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StyleBordersXml.cs @@ -3,7 +3,7 @@ using SpreadCheetah.Styling.Internal; using System.Drawing; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; internal struct StyleBordersXml(List borders, SpreadsheetBuffer buffer) { diff --git a/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StyleCellStylesXml.cs similarity index 98% rename from SpreadCheetah/MetadataXml/StyleCellStylesXml.cs rename to SpreadCheetah/MetadataXml/Styles/StyleCellStylesXml.cs index 1e91d03a..71311f93 100644 --- a/SpreadCheetah/MetadataXml/StyleCellStylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StyleCellStylesXml.cs @@ -1,7 +1,7 @@ using SpreadCheetah.Helpers; using SpreadCheetah.Styling; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; internal struct StyleCellStylesXml( List<(string, StyleId, StyleNameVisibility)>? namedStyles, diff --git a/SpreadCheetah/MetadataXml/StyleFillsXml.cs b/SpreadCheetah/MetadataXml/Styles/StyleFillsXml.cs similarity index 98% rename from SpreadCheetah/MetadataXml/StyleFillsXml.cs rename to SpreadCheetah/MetadataXml/Styles/StyleFillsXml.cs index 292d26cf..bffc0f71 100644 --- a/SpreadCheetah/MetadataXml/StyleFillsXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StyleFillsXml.cs @@ -1,7 +1,7 @@ using SpreadCheetah.Helpers; using SpreadCheetah.Styling.Internal; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; internal struct StyleFillsXml(List fills, SpreadsheetBuffer buffer) { diff --git a/SpreadCheetah/MetadataXml/StyleFontsXml.cs b/SpreadCheetah/MetadataXml/Styles/StyleFontsXml.cs similarity index 98% rename from SpreadCheetah/MetadataXml/StyleFontsXml.cs rename to SpreadCheetah/MetadataXml/Styles/StyleFontsXml.cs index f82d148c..8b760fe1 100644 --- a/SpreadCheetah/MetadataXml/StyleFontsXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StyleFontsXml.cs @@ -2,7 +2,7 @@ using SpreadCheetah.Styling; using SpreadCheetah.Styling.Internal; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; internal struct StyleFontsXml(List fonts, SpreadsheetBuffer buffer) { diff --git a/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs b/SpreadCheetah/MetadataXml/Styles/StyleNumberFormatsXml.cs similarity index 98% rename from SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs rename to SpreadCheetah/MetadataXml/Styles/StyleNumberFormatsXml.cs index 2fa11644..7f3dad16 100644 --- a/SpreadCheetah/MetadataXml/StyleNumberFormatsXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StyleNumberFormatsXml.cs @@ -1,6 +1,6 @@ using SpreadCheetah.Helpers; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; internal struct StyleNumberFormatsXml( List>? customNumberFormats, diff --git a/SpreadCheetah/MetadataXml/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs similarity index 99% rename from SpreadCheetah/MetadataXml/StylesXml.cs rename to SpreadCheetah/MetadataXml/Styles/StylesXml.cs index 8c2301a7..f051376b 100644 --- a/SpreadCheetah/MetadataXml/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -3,7 +3,7 @@ using SpreadCheetah.Styling.Internal; using System.IO.Compression; -namespace SpreadCheetah.MetadataXml; +namespace SpreadCheetah.MetadataXml.Styles; internal struct StylesXml { diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index a41aa761..1258cb7a 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -3,6 +3,7 @@ using SpreadCheetah.Images; using SpreadCheetah.Images.Internal; using SpreadCheetah.MetadataXml; +using SpreadCheetah.MetadataXml.Styles; using SpreadCheetah.SourceGeneration; using SpreadCheetah.Styling; using SpreadCheetah.Styling.Internal; From a5f29a0925671b407699aee09c19bd2cc73ca4ff Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 20:46:34 +0200 Subject: [PATCH 12/42] Rename styles.xml part writers --- .../{StyleBordersXml.cs => BordersXmlPart.cs} | 2 +- ...eCellStylesXml.cs => CellStylesXmlPart.cs} | 2 +- .../{StyleFillsXml.cs => FillsXmlPart.cs} | 2 +- .../{StyleFontsXml.cs => FontsXmlPart.cs} | 2 +- ...rFormatsXml.cs => NumberFormatsXmlPart.cs} | 2 +- SpreadCheetah/MetadataXml/Styles/StylesXml.cs | 20 +++++++++---------- 6 files changed, 15 insertions(+), 15 deletions(-) rename SpreadCheetah/MetadataXml/Styles/{StyleBordersXml.cs => BordersXmlPart.cs} (98%) rename SpreadCheetah/MetadataXml/Styles/{StyleCellStylesXml.cs => CellStylesXmlPart.cs} (98%) rename SpreadCheetah/MetadataXml/Styles/{StyleFillsXml.cs => FillsXmlPart.cs} (96%) rename SpreadCheetah/MetadataXml/Styles/{StyleFontsXml.cs => FontsXmlPart.cs} (97%) rename SpreadCheetah/MetadataXml/Styles/{StyleNumberFormatsXml.cs => NumberFormatsXmlPart.cs} (98%) diff --git a/SpreadCheetah/MetadataXml/Styles/StyleBordersXml.cs b/SpreadCheetah/MetadataXml/Styles/BordersXmlPart.cs similarity index 98% rename from SpreadCheetah/MetadataXml/Styles/StyleBordersXml.cs rename to SpreadCheetah/MetadataXml/Styles/BordersXmlPart.cs index 53f4eaa1..ce751b92 100644 --- a/SpreadCheetah/MetadataXml/Styles/StyleBordersXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/BordersXmlPart.cs @@ -5,7 +5,7 @@ namespace SpreadCheetah.MetadataXml.Styles; -internal struct StyleBordersXml(List borders, SpreadsheetBuffer buffer) +internal struct BordersXmlPart(List borders, SpreadsheetBuffer buffer) { private Element _next; private int _nextIndex; diff --git a/SpreadCheetah/MetadataXml/Styles/StyleCellStylesXml.cs b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs similarity index 98% rename from SpreadCheetah/MetadataXml/Styles/StyleCellStylesXml.cs rename to SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs index 71311f93..6098c261 100644 --- a/SpreadCheetah/MetadataXml/Styles/StyleCellStylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs @@ -3,7 +3,7 @@ namespace SpreadCheetah.MetadataXml.Styles; -internal struct StyleCellStylesXml( +internal struct CellStylesXmlPart( List<(string, StyleId, StyleNameVisibility)>? namedStyles, SpreadsheetBuffer buffer) { diff --git a/SpreadCheetah/MetadataXml/Styles/StyleFillsXml.cs b/SpreadCheetah/MetadataXml/Styles/FillsXmlPart.cs similarity index 96% rename from SpreadCheetah/MetadataXml/Styles/StyleFillsXml.cs rename to SpreadCheetah/MetadataXml/Styles/FillsXmlPart.cs index bffc0f71..82a754ae 100644 --- a/SpreadCheetah/MetadataXml/Styles/StyleFillsXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/FillsXmlPart.cs @@ -3,7 +3,7 @@ namespace SpreadCheetah.MetadataXml.Styles; -internal struct StyleFillsXml(List fills, SpreadsheetBuffer buffer) +internal struct FillsXmlPart(List fills, SpreadsheetBuffer buffer) { private Element _next; private int _nextIndex; diff --git a/SpreadCheetah/MetadataXml/Styles/StyleFontsXml.cs b/SpreadCheetah/MetadataXml/Styles/FontsXmlPart.cs similarity index 97% rename from SpreadCheetah/MetadataXml/Styles/StyleFontsXml.cs rename to SpreadCheetah/MetadataXml/Styles/FontsXmlPart.cs index 8b760fe1..27b23e34 100644 --- a/SpreadCheetah/MetadataXml/Styles/StyleFontsXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/FontsXmlPart.cs @@ -4,7 +4,7 @@ namespace SpreadCheetah.MetadataXml.Styles; -internal struct StyleFontsXml(List fonts, SpreadsheetBuffer buffer) +internal struct FontsXmlPart(List fonts, SpreadsheetBuffer buffer) { private Element _next; private int _nextIndex; diff --git a/SpreadCheetah/MetadataXml/Styles/StyleNumberFormatsXml.cs b/SpreadCheetah/MetadataXml/Styles/NumberFormatsXmlPart.cs similarity index 98% rename from SpreadCheetah/MetadataXml/Styles/StyleNumberFormatsXml.cs rename to SpreadCheetah/MetadataXml/Styles/NumberFormatsXmlPart.cs index 7f3dad16..b9aaa275 100644 --- a/SpreadCheetah/MetadataXml/Styles/StyleNumberFormatsXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/NumberFormatsXmlPart.cs @@ -2,7 +2,7 @@ namespace SpreadCheetah.MetadataXml.Styles; -internal struct StyleNumberFormatsXml( +internal struct NumberFormatsXmlPart( List>? customNumberFormats, SpreadsheetBuffer buffer) { diff --git a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs index f051376b..abe6322c 100644 --- a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -57,11 +57,11 @@ public static async ValueTask WriteAsync( private readonly Dictionary _fills; private readonly Dictionary _fonts; private readonly SpreadsheetBuffer _buffer; - private StyleNumberFormatsXml _numberFormatsXml; - private StyleBordersXml _bordersXml; - private StyleFillsXml _fillsXml; - private StyleFontsXml _fontsXml; - private StyleCellStylesXml _cellStylesXml; + private NumberFormatsXmlPart _numberFormatsXml; + private BordersXmlPart _bordersXml; + private FillsXmlPart _fillsXml; + private FontsXmlPart _fontsXml; + private CellStylesXmlPart _cellStylesXml; private Element _next; private int _nextIndex; @@ -75,11 +75,11 @@ private StylesXml( _fills = CreateFillDictionary(styles); _fonts = CreateFontDictionary(styles); _buffer = buffer; - _numberFormatsXml = new StyleNumberFormatsXml(_customNumberFormats is { } formats ? [.. formats] : null, buffer); - _bordersXml = new StyleBordersXml([.. _borders.Keys], buffer); - _fillsXml = new StyleFillsXml([.. _fills.Keys], buffer); - _fontsXml = new StyleFontsXml([.. _fonts.Keys], buffer); - _cellStylesXml = new StyleCellStylesXml(namedStyles, buffer); + _numberFormatsXml = new NumberFormatsXmlPart(_customNumberFormats is { } formats ? [.. formats] : null, buffer); + _bordersXml = new BordersXmlPart([.. _borders.Keys], buffer); + _fillsXml = new FillsXmlPart([.. _fills.Keys], buffer); + _fontsXml = new FontsXmlPart([.. _fonts.Keys], buffer); + _cellStylesXml = new CellStylesXmlPart(namedStyles, buffer); _styles = styles; } From 0d09e008578190abf9b9309125251cd259bcbe32 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 27 Jul 2024 21:27:52 +0200 Subject: [PATCH 13/42] Move the styles.xml "xf" part writing to a separate class --- SpreadCheetah/MetadataXml/Styles/StylesXml.cs | 104 +-------------- SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs | 120 ++++++++++++++++++ 2 files changed, 123 insertions(+), 101 deletions(-) create mode 100644 SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs diff --git a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs index abe6322c..ddefebaa 100644 --- a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -197,117 +197,19 @@ private readonly bool TryWriteCellXfsStart() private bool TryWriteStyles() { + var xfXml = new XfXmlPart(_buffer, _customNumberFormats, _borders, _fills, _fonts, true); var styles = _styles; for (; _nextIndex < styles.Count; ++_nextIndex) { var style = _styles[_nextIndex]; - var span = _buffer.GetSpan(); - var written = 0; - - var numberFormatId = GetNumberFormatId(style.Format, _customNumberFormats); - - if (!" 0 && !" applyNumberFormat=\"1\""u8.TryCopyTo(span, ref written)) return false; - - if (!TryWriteFont(style.Font, span, ref written)) return false; - if (!TryWriteFill(style.Fill, span, ref written)) return false; - - if (_borders.TryGetValue(style.Border, out var borderIndex) && borderIndex > 0) - { - if (!" borderId=\""u8.TryCopyTo(span, ref written)) return false; - if (!SpanHelper.TryWrite(borderIndex, span, ref written)) return false; - if (!"\" applyBorder=\"1\""u8.TryCopyTo(span, ref written)) return false; - } - - // TODO: For cellXfs entries which are normal styles, xfId should be 0 - // TODO: For cellXfs entires which are named styles, xfId should be the index into cellStyleXfs - // TODO: For cellStyleXfs entries, there should not be xfId - if (!" xfId=\"0\""u8.TryCopyTo(span, ref written)) return false; - - if (style.Alignment == default) - { - if (!"/>"u8.TryCopyTo(span, ref written)) return false; - } - else - { - if (!""" applyAlignment="1">"""u8.TryCopyTo(span, ref written)) return false; - if (!TryWriteAlignment(style.Alignment, span, ref written)) return false; - if (!""u8.TryCopyTo(span, ref written)) return false; - } - - _buffer.Advance(written); + if (!xfXml.TryWrite(style)) + return false; } return true; } - private readonly bool TryWriteFont(ImmutableFont font, Span bytes, ref int bytesWritten) - { - var fontIndex = _fonts.GetValueOrDefault(font); - if (!" fontId=\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; - if (!SpanHelper.TryWrite(fontIndex, bytes, ref bytesWritten)) return false; - if (!"\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; - return fontIndex == 0 || " applyFont=\"1\""u8.TryCopyTo(bytes, ref bytesWritten); - } - - private readonly bool TryWriteFill(ImmutableFill fill, Span bytes, ref int bytesWritten) - { - var fillIndex = _fills.GetValueOrDefault(fill); - if (!" fillId=\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; - if (!SpanHelper.TryWrite(fillIndex, bytes, ref bytesWritten)) return false; - if (!"\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; - return fillIndex == 0 || " applyFill=\"1\""u8.TryCopyTo(bytes, ref bytesWritten); - } - - private static int GetNumberFormatId(NumberFormat? numberFormat, IReadOnlyDictionary? customNumberFormats) - { - if (numberFormat is not { } format) return 0; - if (format.StandardFormat is { } standardFormat) return (int)standardFormat; - if (format.CustomFormat is null) return 0; - return customNumberFormats?.GetValueOrDefault(format.CustomFormat) ?? 0; - } - - private static bool TryWriteAlignment(ImmutableAlignment alignment, Span bytes, ref int bytesWritten) - { - var span = bytes.Slice(bytesWritten); - var written = 0; - - if (!" 0) - { - if (!" indent=\""u8.TryCopyTo(span, ref written)) return false; - if (!SpanHelper.TryWrite(alignment.Indent, span, ref written)) return false; - if (!"\""u8.TryCopyTo(span, ref written)) return false; - } - - if (!"/>"u8.TryCopyTo(span, ref written)) return false; - - bytesWritten += written; - return true; - } - - private static bool TryWriteHorizontalAlignment(HorizontalAlignment alignment, Span bytes, ref int bytesWritten) => alignment switch - { - HorizontalAlignment.Left => " horizontal=\"left\""u8.TryCopyTo(bytes, ref bytesWritten), - HorizontalAlignment.Center => " horizontal=\"center\""u8.TryCopyTo(bytes, ref bytesWritten), - HorizontalAlignment.Right => " horizontal=\"right\""u8.TryCopyTo(bytes, ref bytesWritten), - _ => true - }; - - private static bool TryWriteVerticalAlignment(VerticalAlignment alignment, Span bytes, ref int bytesWritten) => alignment switch - { - VerticalAlignment.Center => " vertical=\"center\""u8.TryCopyTo(bytes, ref bytesWritten), - VerticalAlignment.Top => " vertical=\"top\""u8.TryCopyTo(bytes, ref bytesWritten), - _ => true - }; - private enum Element { Header, diff --git a/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs new file mode 100644 index 00000000..67616e3c --- /dev/null +++ b/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs @@ -0,0 +1,120 @@ +using SpreadCheetah.Helpers; +using SpreadCheetah.Styling; +using SpreadCheetah.Styling.Internal; + +namespace SpreadCheetah.MetadataXml.Styles; + +internal readonly struct XfXmlPart( + SpreadsheetBuffer buffer, + Dictionary? customNumberFormats, + Dictionary borders, + Dictionary fills, + Dictionary fonts, + bool cellXfsEntry) +{ + public bool TryWrite(in ImmutableStyle style) + { + var span = buffer.GetSpan(); + var written = 0; + + var numberFormatId = GetNumberFormatId(style.Format, customNumberFormats); + + if (!" 0 && !" applyNumberFormat=\"1\""u8.TryCopyTo(span, ref written)) return false; + + if (!TryWriteFont(style.Font, span, ref written)) return false; + if (!TryWriteFill(style.Fill, span, ref written)) return false; + + if (borders.TryGetValue(style.Border, out var borderIndex) && borderIndex > 0) + { + if (!" borderId=\""u8.TryCopyTo(span, ref written)) return false; + if (!SpanHelper.TryWrite(borderIndex, span, ref written)) return false; + if (!"\" applyBorder=\"1\""u8.TryCopyTo(span, ref written)) return false; + } + + // TODO: For cellXfs entries which are normal styles, xfId should be 0 + // TODO: For cellXfs entires which are named styles, xfId should be the index into cellStyleXfs + // TODO: For cellStyleXfs entries, there should not be xfId + if (cellXfsEntry && !" xfId=\"0\""u8.TryCopyTo(span, ref written)) return false; + + if (style.Alignment == default) + { + if (!"/>"u8.TryCopyTo(span, ref written)) return false; + } + else + { + if (!""" applyAlignment="1">"""u8.TryCopyTo(span, ref written)) return false; + if (!TryWriteAlignment(style.Alignment, span, ref written)) return false; + if (!""u8.TryCopyTo(span, ref written)) return false; + } + + buffer.Advance(written); + return true; + } + + private readonly bool TryWriteFont(ImmutableFont font, Span bytes, ref int bytesWritten) + { + var fontIndex = fonts.GetValueOrDefault(font); + if (!" fontId=\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; + if (!SpanHelper.TryWrite(fontIndex, bytes, ref bytesWritten)) return false; + if (!"\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; + return fontIndex == 0 || " applyFont=\"1\""u8.TryCopyTo(bytes, ref bytesWritten); + } + + private readonly bool TryWriteFill(ImmutableFill fill, Span bytes, ref int bytesWritten) + { + var fillIndex = fills.GetValueOrDefault(fill); + if (!" fillId=\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; + if (!SpanHelper.TryWrite(fillIndex, bytes, ref bytesWritten)) return false; + if (!"\""u8.TryCopyTo(bytes, ref bytesWritten)) return false; + return fillIndex == 0 || " applyFill=\"1\""u8.TryCopyTo(bytes, ref bytesWritten); + } + + private static int GetNumberFormatId(NumberFormat? numberFormat, Dictionary? customNumberFormats) + { + if (numberFormat is not { } format) return 0; + if (format.StandardFormat is { } standardFormat) return (int)standardFormat; + if (format.CustomFormat is null) return 0; + return customNumberFormats?.GetValueOrDefault(format.CustomFormat) ?? 0; + } + + private static bool TryWriteAlignment(ImmutableAlignment alignment, Span bytes, ref int bytesWritten) + { + var span = bytes.Slice(bytesWritten); + var written = 0; + + if (!" 0) + { + if (!" indent=\""u8.TryCopyTo(span, ref written)) return false; + if (!SpanHelper.TryWrite(alignment.Indent, span, ref written)) return false; + if (!"\""u8.TryCopyTo(span, ref written)) return false; + } + + if (!"/>"u8.TryCopyTo(span, ref written)) return false; + + bytesWritten += written; + return true; + } + + private static bool TryWriteHorizontalAlignment(HorizontalAlignment alignment, Span bytes, ref int bytesWritten) => alignment switch + { + HorizontalAlignment.Left => " horizontal=\"left\""u8.TryCopyTo(bytes, ref bytesWritten), + HorizontalAlignment.Center => " horizontal=\"center\""u8.TryCopyTo(bytes, ref bytesWritten), + HorizontalAlignment.Right => " horizontal=\"right\""u8.TryCopyTo(bytes, ref bytesWritten), + _ => true + }; + + private static bool TryWriteVerticalAlignment(VerticalAlignment alignment, Span bytes, ref int bytesWritten) => alignment switch + { + VerticalAlignment.Center => " vertical=\"center\""u8.TryCopyTo(bytes, ref bytesWritten), + VerticalAlignment.Top => " vertical=\"top\""u8.TryCopyTo(bytes, ref bytesWritten), + _ => true + }; +} From 744d801d1958a6e07963cd8a7f009f076bda176c Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 28 Jul 2024 23:01:08 +0200 Subject: [PATCH 14/42] Write cellStyleXfs entries for named styles in styles.xml --- .../MetadataXml/Styles/CellStylesXmlPart.cs | 3 +- SpreadCheetah/MetadataXml/Styles/StylesXml.cs | 88 ++++++++++++++----- SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs | 1 + SpreadCheetah/Spreadsheet.cs | 3 + 4 files changed, 74 insertions(+), 21 deletions(-) diff --git a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs index 6098c261..b427c414 100644 --- a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs +++ b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs @@ -1,10 +1,11 @@ using SpreadCheetah.Helpers; using SpreadCheetah.Styling; +using SpreadCheetah.Styling.Internal; namespace SpreadCheetah.MetadataXml.Styles; internal struct CellStylesXmlPart( - List<(string, StyleId, StyleNameVisibility)>? namedStyles, + List<(string, ImmutableStyle, StyleNameVisibility)>? namedStyles, SpreadsheetBuffer buffer) { private string? _currentXmlEncodedName; diff --git a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs index ddefebaa..4af27afd 100644 --- a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -1,4 +1,3 @@ -using SpreadCheetah.Helpers; using SpreadCheetah.Styling; using SpreadCheetah.Styling.Internal; using System.IO.Compression; @@ -26,11 +25,7 @@ public static async ValueTask WriteAsync( // The order of Dictionary.Keys is not guaranteed, so we make sure the styles are sorted by the StyleId here. var orderedStyles = styles.OrderBy(x => x.Value).Select(x => x.Key).ToList(); - var filteredNamedStyles = namedStyles? - .Where(x => x.Value.Item2 is not null) - .Select(x => (x.Key, x.Value.Item1, x.Value.Item2.GetValueOrDefault())) - .ToList(); - + var filteredNamedStyles = FilterNamedStyles(styles, namedStyles); var writer = new StylesXml(orderedStyles, filteredNamedStyles, buffer); foreach (var success in writer) @@ -43,6 +38,34 @@ public static async ValueTask WriteAsync( } } + private static List<(string, ImmutableStyle, StyleNameVisibility)>? FilterNamedStyles( + Dictionary styles, + Dictionary? namedStyles) + { + if (namedStyles is null) + return null; + + List<(string, ImmutableStyle, StyleNameVisibility)>? result = null; + Dictionary? styleIdToStyle = null; + + foreach (var (name, value) in namedStyles) + { + if (value.Visibility is not { } visibility) + continue; + + styleIdToStyle ??= styles.ToDictionary(x => x.Value, x => x.Key); + + var styleId = value.StyleId.Id; + if (!styleIdToStyle.TryGetValue(styleId, out var style)) + continue; + + result ??= []; + result.Add((name, style, visibility)); + } + + return result; + } + private static ReadOnlySpan Header => """"""u8 + """"""u8; @@ -52,6 +75,7 @@ public static async ValueTask WriteAsync( """"""u8; private readonly List _styles; + private readonly List<(string, ImmutableStyle, StyleNameVisibility)>? _namedStyles; private readonly Dictionary? _customNumberFormats; private readonly Dictionary _borders; private readonly Dictionary _fills; @@ -67,7 +91,7 @@ public static async ValueTask WriteAsync( private StylesXml( List styles, - List<(string, StyleId, StyleNameVisibility)>? namedStyles, + List<(string, ImmutableStyle, StyleNameVisibility)>? namedStyles, SpreadsheetBuffer buffer) { _customNumberFormats = CreateCustomNumberFormatDictionary(styles); @@ -81,6 +105,7 @@ private StylesXml( _fontsXml = new FontsXmlPart([.. _fonts.Keys], buffer); _cellStylesXml = new CellStylesXmlPart(namedStyles, buffer); _styles = styles; + _namedStyles = namedStyles; } public readonly StylesXml GetEnumerator() => this; @@ -165,8 +190,10 @@ public bool MoveNext() Element.Fonts => _fontsXml.TryWrite(), Element.Fills => _fillsXml.TryWrite(), Element.Borders => _bordersXml.TryWrite(), + Element.CellStyleXfsStart => TryWriteCellStyleXfsStart(), + Element.CellStyleXfsEntries => TryWriteCellStyleXfsEntries(), Element.CellXfsStart => TryWriteCellXfsStart(), - Element.Styles => TryWriteStyles(), + Element.CellXfsEntries => TryWriteCellXfsEntries(), Element.CellXfsEnd => _buffer.TryWrite(""u8), Element.CellStyles => _cellStylesXml.TryWrite(), _ => _buffer.TryWrite(Footer), @@ -178,24 +205,42 @@ public bool MoveNext() return _next < Element.Done; } - private readonly bool TryWriteCellXfsStart() + private readonly bool TryWriteCellStyleXfsStart() + { + var count = (_namedStyles?.Count ?? 0) + 1; + return _buffer.TryWrite( + $"{""u8}"); + } + + private bool TryWriteCellStyleXfsEntries() { - var span = _buffer.GetSpan(); - var written = 0; + if (_namedStyles is not { } namedStyles) + return true; - ReadOnlySpan xml = - """"""u8 + - ""u8.TryCopyTo(span, ref written)) return false; + for (; _nextIndex < _namedStyles.Count; ++_nextIndex) + { + var (_, style, _) = namedStyles[_nextIndex]; + if (!xfXml.TryWrite(style)) + return false; + } - _buffer.Advance(written); + _nextIndex = 0; return true; } - private bool TryWriteStyles() + private readonly bool TryWriteCellXfsStart() + { + return _buffer.TryWrite( + $"{""u8}"); + } + + private bool TryWriteCellXfsEntries() { var xfXml = new XfXmlPart(_buffer, _customNumberFormats, _borders, _fills, _fonts, true); var styles = _styles; @@ -207,6 +252,7 @@ private bool TryWriteStyles() return false; } + _nextIndex = 0; return true; } @@ -217,8 +263,10 @@ private enum Element Fonts, Fills, Borders, + CellStyleXfsStart, + CellStyleXfsEntries, CellXfsStart, - Styles, + CellXfsEntries, CellXfsEnd, CellStyles, Footer, diff --git a/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs index 67616e3c..2bb3a0e7 100644 --- a/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs +++ b/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs @@ -36,6 +36,7 @@ public bool TryWrite(in ImmutableStyle style) // TODO: For cellXfs entries which are normal styles, xfId should be 0 // TODO: For cellXfs entires which are named styles, xfId should be the index into cellStyleXfs + // TODO: -> What about DateTime cells with a default style? // TODO: For cellStyleXfs entries, there should not be xfId if (cellXfsEntry && !" xfId=\"0\""u8.TryCopyTo(span, ref written)) return false; diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index 1258cb7a..37459ce0 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -410,6 +410,9 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName if (namedStyles.ContainsKey(name)) ThrowHelper.StyleNameAlreadyExists(nameof(name)); + // TODO: When there is a default DateTime number format, two style IDs will be created. + // TODO: How should this be handled for a named style? + // TODO: Maybe the named style should only refer to the regular style, not the DateTime style. var styleId = AddStyle(style); namedStyles[name] = (styleId, styleNameVisibility); From 2ea8166cbfb2655c4fcde94117c84de25dfba0e6 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 4 Aug 2024 22:26:15 +0200 Subject: [PATCH 15/42] Handle hidden named styles --- SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs index b427c414..dae5c469 100644 --- a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs +++ b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs @@ -59,15 +59,16 @@ private bool TryWriteCellStyles() for (; _nextIndex < namedStylesLocal.Count; ++_nextIndex) { - var (name, _, _) = namedStylesLocal[_nextIndex]; + var (name, _, visibility) = namedStylesLocal[_nextIndex]; var span = buffer.GetSpan(); var written = 0; - // TODO: Handle "hidden" + // TODO: Ensure "hidden" works as expected in Excel/Calc if (_currentXmlEncodedName is null) { if (!" Date: Sun, 4 Aug 2024 22:32:22 +0200 Subject: [PATCH 16/42] Add StyleNameVisibility to package validation suppression --- SpreadCheetah/CompatibilitySuppressions.xml | 34 ++++----------------- SpreadCheetah/SpreadCheetah.csproj | 2 +- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/SpreadCheetah/CompatibilitySuppressions.xml b/SpreadCheetah/CompatibilitySuppressions.xml index 7769d601..4ed57789 100644 --- a/SpreadCheetah/CompatibilitySuppressions.xml +++ b/SpreadCheetah/CompatibilitySuppressions.xml @@ -1,34 +1,6 @@  - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/net6.0/SpreadCheetah.dll - lib/net6.0/SpreadCheetah.dll - true - - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/net7.0/SpreadCheetah.dll - lib/net7.0/SpreadCheetah.dll - true - - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/net8.0/SpreadCheetah.dll - lib/net8.0/SpreadCheetah.dll - true - - - CP0002 - M:SpreadCheetah.SourceGeneration.WorksheetRowMetadataServices.CreateObjectInfo``1(System.Func{SpreadCheetah.Spreadsheet,SpreadCheetah.Styling.StyleId,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,``0,System.Threading.CancellationToken,System.Threading.Tasks.ValueTask},System.Func{SpreadCheetah.Spreadsheet,System.Collections.Generic.IEnumerable{``0},System.Threading.CancellationToken,System.Threading.Tasks.ValueTask}) - lib/netstandard2.0/SpreadCheetah.dll - lib/netstandard2.0/SpreadCheetah.dll - true - CP0008 T:SpreadCheetah.SourceGeneration.InheritedColumnsOrder @@ -65,6 +37,12 @@ lib/net7.0/SpreadCheetah.dll lib/net8.0/SpreadCheetah.dll + + CP0008 + T:SpreadCheetah.Styling.StyleNameVisibility + lib/net7.0/SpreadCheetah.dll + lib/net8.0/SpreadCheetah.dll + CP0008 T:SpreadCheetah.Styling.VerticalAlignment diff --git a/SpreadCheetah/SpreadCheetah.csproj b/SpreadCheetah/SpreadCheetah.csproj index ae3d88af..86a9dcd3 100644 --- a/SpreadCheetah/SpreadCheetah.csproj +++ b/SpreadCheetah/SpreadCheetah.csproj @@ -34,7 +34,7 @@ true - 1.15.0 + 1.16.0 true true From abdde8a35bb66f8a975eeda6b270aee0db6dd000 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sun, 4 Aug 2024 23:15:34 +0200 Subject: [PATCH 17/42] Validate style name --- SpreadCheetah/Helpers/StringExtensions.cs | 2 +- SpreadCheetah/Helpers/ThrowHelper.cs | 12 +++++++++--- SpreadCheetah/Spreadsheet.cs | 11 ++++++++--- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/SpreadCheetah/Helpers/StringExtensions.cs b/SpreadCheetah/Helpers/StringExtensions.cs index 8f9fac38..c86fdb80 100644 --- a/SpreadCheetah/Helpers/StringExtensions.cs +++ b/SpreadCheetah/Helpers/StringExtensions.cs @@ -19,7 +19,7 @@ value is not null && value.Length > maxLength public static void EnsureValidWorksheetName(this string name, [CallerArgumentExpression(nameof(name))] string? paramName = null) { if (string.IsNullOrWhiteSpace(name)) - ThrowHelper.WorksheetNameEmptyOrWhiteSpace(paramName); + ThrowHelper.NameEmptyOrWhiteSpace(paramName); if (name.Length > 31) ThrowHelper.WorksheetNameTooLong(paramName); diff --git a/SpreadCheetah/Helpers/ThrowHelper.cs b/SpreadCheetah/Helpers/ThrowHelper.cs index e7f195ec..a838226f 100644 --- a/SpreadCheetah/Helpers/ThrowHelper.cs +++ b/SpreadCheetah/Helpers/ThrowHelper.cs @@ -49,6 +49,9 @@ internal static class ThrowHelper [DoesNotReturn] public static void ImageScaleTooSmall(string? paramName, float actualValue) => throw new ArgumentOutOfRangeException(paramName, actualValue, "The image scale must result in image width and height being at least 1 pixel."); + [DoesNotReturn] + public static void NameEmptyOrWhiteSpace(string? paramName) => throw new ArgumentException("The name can not be empty or consist only of whitespace.", paramName); + [DoesNotReturn] public static void NoteTextTooLong(string? paramName) => throw new ArgumentException($"Note text can not exceed {SpreadsheetConstants.MaxNoteTextLength} characters.", paramName); @@ -76,9 +79,15 @@ internal static class ThrowHelper [DoesNotReturn] public static void StyleNameAlreadyExists(string? paramName) => throw new ArgumentException("A style with the given name already exists.", paramName); + [DoesNotReturn] + public static void StyleNameCanNotEqualNormal(string? paramName) => throw new ArgumentException("The name can not be equal to 'Normal', since that is the name of the default built-in style.", paramName); + [DoesNotReturn] public static void StyleNameNotFound(string name) => throw new SpreadCheetahException($"Style with name '{name}' was not found. Make sure the style is first added to the spreadsheet by calling {nameof(Spreadsheet.AddStyle)} with a name argument."); + [DoesNotReturn] + public static void StyleNameStartsOrEndsWithWhiteSpace(string? paramName) => throw new ArgumentException("The name can not start or end with white-space.", paramName); + [DoesNotReturn] public static void StyleNameTooLong(string? paramName) => throw new ArgumentException("The name can not be more than 255 characters.", paramName); @@ -88,9 +97,6 @@ internal static class ThrowHelper [DoesNotReturn] public static void WorksheetNameAlreadyExists(string? paramName) => throw new ArgumentException("A worksheet with the given name already exists.", paramName); - [DoesNotReturn] - public static void WorksheetNameEmptyOrWhiteSpace(string? paramName) => throw new ArgumentException("The name can not be empty or consist only of whitespace.", paramName); - [DoesNotReturn] public static void WorksheetNameInvalidCharacters(string? paramName, string invalidChars) => throw new ArgumentException("The name can not contain any of the following characters: " + invalidChars, paramName); diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index 37459ce0..aeab114c 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -401,12 +401,17 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName ArgumentNullException.ThrowIfNull(style); ArgumentNullException.ThrowIfNull(name); + if (string.IsNullOrWhiteSpace(name)) + ThrowHelper.NameEmptyOrWhiteSpace(nameof(name)); if (name.Length > 255) ThrowHelper.StyleNameTooLong(nameof(name)); + if (char.IsWhiteSpace(name[0]) || char.IsWhiteSpace(name[^1])) + ThrowHelper.StyleNameStartsOrEndsWithWhiteSpace(nameof(name)); + if (name.Equals("Normal", StringComparison.OrdinalIgnoreCase)) + ThrowHelper.StyleNameCanNotEqualNormal(nameof(name)); - // TODO: Validate uniqueness of name (does case sensitivity matter? does whitespace matter?) - // TODO: Can the name "Normal" be used? - var namedStyles = _namedStyles ??= []; + // TODO: Test duplicate name but different casing + var namedStyles = _namedStyles ??= new(StringComparer.OrdinalIgnoreCase); if (namedStyles.ContainsKey(name)) ThrowHelper.StyleNameAlreadyExists(nameof(name)); From 5e49a22804c6432ab82a58baee4b07057628ccbe Mon Sep 17 00:00:00 2001 From: sveinungf Date: Fri, 9 Aug 2024 19:30:35 +0200 Subject: [PATCH 18/42] Handle styles with a StyleManager --- SpreadCheetah/MetadataXml/Styles/StylesXml.cs | 39 +-------- SpreadCheetah/Spreadsheet.cs | 48 ++++------- .../Styling/Internal/StyleManager.cs | 84 +++++++++++++++++++ 3 files changed, 103 insertions(+), 68 deletions(-) create mode 100644 SpreadCheetah/Styling/Internal/StyleManager.cs diff --git a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs index 4af27afd..78c09eca 100644 --- a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -10,8 +10,7 @@ public static async ValueTask WriteAsync( ZipArchive archive, CompressionLevel compressionLevel, SpreadsheetBuffer buffer, - Dictionary styles, - Dictionary? namedStyles, + StyleManager styleManager, CancellationToken token) { var entry = archive.CreateEntry("xl/styles.xml", compressionLevel); @@ -22,11 +21,9 @@ public static async ValueTask WriteAsync( await using (stream.ConfigureAwait(false)) #endif { - // The order of Dictionary.Keys is not guaranteed, so we make sure the styles are sorted by the StyleId here. - var orderedStyles = styles.OrderBy(x => x.Value).Select(x => x.Key).ToList(); - - var filteredNamedStyles = FilterNamedStyles(styles, namedStyles); - var writer = new StylesXml(orderedStyles, filteredNamedStyles, buffer); + var orderedStyles = styleManager.GetOrderedStyles(); + var embeddedNamedStyles = styleManager.GetEmbeddedNamedStyles(); + var writer = new StylesXml(orderedStyles, embeddedNamedStyles, buffer); foreach (var success in writer) { @@ -38,34 +35,6 @@ public static async ValueTask WriteAsync( } } - private static List<(string, ImmutableStyle, StyleNameVisibility)>? FilterNamedStyles( - Dictionary styles, - Dictionary? namedStyles) - { - if (namedStyles is null) - return null; - - List<(string, ImmutableStyle, StyleNameVisibility)>? result = null; - Dictionary? styleIdToStyle = null; - - foreach (var (name, value) in namedStyles) - { - if (value.Visibility is not { } visibility) - continue; - - styleIdToStyle ??= styles.ToDictionary(x => x.Value, x => x.Key); - - var styleId = value.StyleId.Id; - if (!styleIdToStyle.TryGetValue(styleId, out var style)) - continue; - - result ??= []; - result.Add((name, style, visibility)); - } - - return result; - } - private static ReadOnlySpan Header => """"""u8 + """"""u8; diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index aeab114c..076c67fc 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -31,9 +31,8 @@ public sealed class Spreadsheet : IDisposable, IAsyncDisposable private readonly bool _writeCellReferenceAttributes; private readonly NumberFormat? _defaultDateTimeFormat; private DefaultStyling? _defaultStyling; - private Dictionary? _styles; - private Dictionary? _namedStyles; private FileCounter? _fileCounter; + private StyleManager? _styleManager; private Worksheet? _worksheet; private bool _disposed; private bool _finished; @@ -390,7 +389,7 @@ public StyleId AddStyle(Style style) ArgumentNullException.ThrowIfNull(style); // If we have any style, the built-in default style must be the first one (meaning the first element in styles.xml). - if (_styles is null) + if (_styleManager is null) AddDefaultStyle(); return AddStyle(ImmutableStyle.From(style)); @@ -411,15 +410,17 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName ThrowHelper.StyleNameCanNotEqualNormal(nameof(name)); // TODO: Test duplicate name but different casing - var namedStyles = _namedStyles ??= new(StringComparer.OrdinalIgnoreCase); - if (namedStyles.ContainsKey(name)) + var styleManager = _styleManager ??= new(); + if (styleManager.StyleNameExists(name)) ThrowHelper.StyleNameAlreadyExists(nameof(name)); // TODO: When there is a default DateTime number format, two style IDs will be created. // TODO: How should this be handled for a named style? // TODO: Maybe the named style should only refer to the regular style, not the DateTime style. var styleId = AddStyle(style); - namedStyles[name] = (styleId, styleNameVisibility); + + // TODO: The styleManager can also handle the default style? + styleManager.AddNamedStyle(name, styleId, styleNameVisibility); return styleId; } @@ -436,28 +437,8 @@ private void AddDefaultStyle() private StyleId AddStyle(in ImmutableStyle style) { - // Optionally add another style for DateTime when there is no explicit number format in the new style. - ImmutableStyle? dateTimeStyle = _defaultDateTimeFormat is not null && style.Format is null - ? style with { Format = _defaultDateTimeFormat } - : null; - - _styles ??= []; - if (_styles.TryGetValue(style, out var id)) - { - return dateTimeStyle is { } dtStyle && _styles.TryGetValue(dtStyle, out var dateTimeId) - ? new StyleId(id, dateTimeId) - : new StyleId(id, id); - } - - var newId = _styles.Count; - _styles[style] = newId; - - if (dateTimeStyle is not { } dateTimeStyleValue) - return new StyleId(newId, newId); - - var newDateTimeId = newId + 1; - _styles[dateTimeStyleValue] = newDateTimeId; - return new StyleId(newId, newDateTimeId); + _styleManager ??= new(); + return _styleManager.AddStyleIfNotExists(style, _defaultDateTimeFormat); } /// @@ -469,8 +450,9 @@ public StyleId GetStyleId(string name) { ArgumentNullException.ThrowIfNull(name); - if (_namedStyles is { } namedStyles && namedStyles.TryGetValue(name, out var value)) - return value.Item1; + var styleId = _styleManager?.GetStyleIdOrDefault(name); + if (styleId is not null) + return styleId; ThrowHelper.StyleNameNotFound(name); return null; // Unreachable @@ -655,14 +637,14 @@ private async ValueTask FinishInternalAsync(CancellationToken token) { await FinishAndDisposeWorksheetAsync(token).ConfigureAwait(false); - var hasStyles = _styles != null; + var hasStyles = _styleManager is not null; await ContentTypesXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, _fileCounter, hasStyles, token).ConfigureAwait(false); await WorkbookRelsXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, hasStyles, token).ConfigureAwait(false); await WorkbookXml.WriteAsync(_archive, _compressionLevel, _buffer, _worksheets, token).ConfigureAwait(false); - if (_styles is not null) - await StylesXml.WriteAsync(_archive, _compressionLevel, _buffer, _styles, _namedStyles, token).ConfigureAwait(false); + if (_styleManager is not null) + await StylesXml.WriteAsync(_archive, _compressionLevel, _buffer, _styleManager, token).ConfigureAwait(false); _finished = true; diff --git a/SpreadCheetah/Styling/Internal/StyleManager.cs b/SpreadCheetah/Styling/Internal/StyleManager.cs new file mode 100644 index 00000000..697a20c7 --- /dev/null +++ b/SpreadCheetah/Styling/Internal/StyleManager.cs @@ -0,0 +1,84 @@ +namespace SpreadCheetah.Styling.Internal; + +internal sealed class StyleManager +{ + private readonly record struct StyleIdentifiers( + int StyleId); + + private readonly record struct EmbeddedNamedStyle( + StyleNameVisibility Visibility); + + private readonly Dictionary _styles = []; + private Dictionary? _namedStyles; + private Dictionary? _embeddedNamedStyles; + + public StyleId AddStyleIfNotExists(in ImmutableStyle style, NumberFormat? defaultDateTimeFormat) + { + var id = AddStyleIfNotExistsInternal(style); + + if (defaultDateTimeFormat is null || style.Format is not null) + return new StyleId(id, id); + + // Optionally add another style for DateTime when there is no explicit number format in the new style. + var dateTimeStyle = style with { Format = defaultDateTimeFormat }; + var dateTimeId = AddStyleIfNotExistsInternal(dateTimeStyle); + + return new StyleId(id, dateTimeId); + } + + private int AddStyleIfNotExistsInternal(in ImmutableStyle style) + { + if (_styles.TryGetValue(style, out var existingIdentifiers)) + return existingIdentifiers.StyleId; + + var id = _styles.Count; + _styles[style] = new StyleIdentifiers(id); + return id; + } + + public bool StyleNameExists(string name) + { + return _namedStyles is { } namedStyles && namedStyles.ContainsKey(name); + } + + public void AddNamedStyle(string name, StyleId styleId, StyleNameVisibility? visibility) + { + var namedStyles = _namedStyles ??= new(StringComparer.OrdinalIgnoreCase); + namedStyles[name] = styleId; + + if (visibility is { } vis) + { + var embeddedNamedStyles = _embeddedNamedStyles ??= new(StringComparer.OrdinalIgnoreCase); + embeddedNamedStyles[name] = new EmbeddedNamedStyle(vis); + } + } + + public StyleId? GetStyleIdOrDefault(string name) => _namedStyles?.GetValueOrDefault(name); + + public List GetOrderedStyles() + { + // The order of Dictionary.Keys is not guaranteed, so we make sure the styles are sorted by the StyleId here. + return _styles.OrderBy(x => x.Value.StyleId).Select(x => x.Key).ToList(); + } + + public List<(string, ImmutableStyle, StyleNameVisibility)>? GetEmbeddedNamedStyles() + { + if (_embeddedNamedStyles is null || _namedStyles is null) + return null; + + var result = new List<(string, ImmutableStyle, StyleNameVisibility)>(_embeddedNamedStyles.Count); + var styleIdToStyle = _styles.ToDictionary(x => x.Value.StyleId, x => x.Key); + + foreach (var (name, embeddedNameStyle) in _embeddedNamedStyles) + { + if (!_namedStyles.TryGetValue(name, out var styleId)) + continue; + if (!styleIdToStyle.TryGetValue(styleId.Id, out var style)) + continue; + + result.Add((name, style, embeddedNameStyle.Visibility)); + } + + return result; + } +} From b9c6554553e0ef56e2aaf50ecf144688d18a32d7 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 14:34:58 +0200 Subject: [PATCH 19/42] Move the DefaultStyling field to StyleManager --- SpreadCheetah/Spreadsheet.cs | 11 +++-------- SpreadCheetah/Styling/Internal/DefaultStyling.cs | 10 +--------- SpreadCheetah/Styling/Internal/StyleManager.cs | 12 ++++++++++++ 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index 076c67fc..9fb976fd 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -30,7 +30,6 @@ public sealed class Spreadsheet : IDisposable, IAsyncDisposable private readonly SpreadsheetBuffer _buffer; private readonly bool _writeCellReferenceAttributes; private readonly NumberFormat? _defaultDateTimeFormat; - private DefaultStyling? _defaultStyling; private FileCounter? _fileCounter; private StyleManager? _styleManager; private Worksheet? _worksheet; @@ -154,7 +153,7 @@ private async ValueTask StartWorksheetInternalAsync(string name, WorksheetOption var path = StringHelper.Invariant($"xl/worksheets/sheet{_worksheets.Count + 1}.xml"); var entry = _archive.CreateEntry(path, _compressionLevel); var entryStream = entry.Open(); - _worksheet = new Worksheet(entryStream, _defaultStyling, _buffer, _writeCellReferenceAttributes); + _worksheet = new Worksheet(entryStream, _styleManager?.DefaultStyling, _buffer, _writeCellReferenceAttributes); await _worksheet.WriteHeadAsync(options, token).ConfigureAwait(false); _worksheets.Add(new WorksheetMetadata(name, path, options?.Visibility ?? WorksheetVisibility.Visible)); } @@ -427,12 +426,8 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName private void AddDefaultStyle() { - var defaultFont = new ImmutableFont(null, false, false, false, Font.DefaultSize, null); - var defaultStyle = new ImmutableStyle(new ImmutableAlignment(), new ImmutableBorder(), new ImmutableFill(), defaultFont, null); - var styleId = AddStyle(defaultStyle); - - if (styleId.Id != styleId.DateTimeId) - _defaultStyling = new DefaultStyling(styleId.DateTimeId); + var styleManager = _styleManager ??= new(); + styleManager.AddDefaultStyle(_defaultDateTimeFormat); } private StyleId AddStyle(in ImmutableStyle style) diff --git a/SpreadCheetah/Styling/Internal/DefaultStyling.cs b/SpreadCheetah/Styling/Internal/DefaultStyling.cs index 00d03ed5..ba58e617 100644 --- a/SpreadCheetah/Styling/Internal/DefaultStyling.cs +++ b/SpreadCheetah/Styling/Internal/DefaultStyling.cs @@ -1,11 +1,3 @@ namespace SpreadCheetah.Styling.Internal; -internal sealed class DefaultStyling -{ - public int? DateTimeStyleId { get; } - - public DefaultStyling(int? dateTimeStyleId) - { - DateTimeStyleId = dateTimeStyleId; - } -} +internal sealed record DefaultStyling(int? DateTimeStyleId); \ No newline at end of file diff --git a/SpreadCheetah/Styling/Internal/StyleManager.cs b/SpreadCheetah/Styling/Internal/StyleManager.cs index 697a20c7..df78beb0 100644 --- a/SpreadCheetah/Styling/Internal/StyleManager.cs +++ b/SpreadCheetah/Styling/Internal/StyleManager.cs @@ -12,6 +12,8 @@ private readonly record struct EmbeddedNamedStyle( private Dictionary? _namedStyles; private Dictionary? _embeddedNamedStyles; + public DefaultStyling? DefaultStyling { get; private set; } + public StyleId AddStyleIfNotExists(in ImmutableStyle style, NumberFormat? defaultDateTimeFormat) { var id = AddStyleIfNotExistsInternal(style); @@ -81,4 +83,14 @@ public List GetOrderedStyles() return result; } + + public void AddDefaultStyle(NumberFormat? defaultDateTimeFormat) + { + var defaultFont = new ImmutableFont(null, false, false, false, Font.DefaultSize, null); + var defaultStyle = new ImmutableStyle(new ImmutableAlignment(), new ImmutableBorder(), new ImmutableFill(), defaultFont, null); + var styleId = AddStyleIfNotExists(defaultStyle, defaultDateTimeFormat); + + if (styleId.Id != styleId.DateTimeId) + DefaultStyling = new DefaultStyling(styleId.DateTimeId); + } } From 79d10f56d3383cf6afabb4db86615faffda06c19 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 15:54:58 +0200 Subject: [PATCH 20/42] Handle the default DateTime format in StyleManager --- SpreadCheetah/Spreadsheet.cs | 35 +++++-------------- .../Styling/Internal/StyleManager.cs | 32 +++++++++-------- 2 files changed, 26 insertions(+), 41 deletions(-) diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index 9fb976fd..f15408bf 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -29,7 +29,6 @@ public sealed class Spreadsheet : IDisposable, IAsyncDisposable private readonly CompressionLevel _compressionLevel; private readonly SpreadsheetBuffer _buffer; private readonly bool _writeCellReferenceAttributes; - private readonly NumberFormat? _defaultDateTimeFormat; private FileCounter? _fileCounter; private StyleManager? _styleManager; private Worksheet? _worksheet; @@ -50,8 +49,12 @@ private Spreadsheet(ZipArchive archive, CompressionLevel compressionLevel, int b _archive = archive; _compressionLevel = compressionLevel; _buffer = new SpreadsheetBuffer(bufferSize); - _defaultDateTimeFormat = defaultDateTimeFormat; _writeCellReferenceAttributes = writeCellReferenceAttributes; + + // If no style is ever added to the spreadsheet, then we can skip creating the styles.xml file. + // If we have any style, the built-in default style must be the first one (meaning the first element in styles.xml). + if (defaultDateTimeFormat is { } format) + _styleManager = new(format); } /// @@ -70,12 +73,6 @@ public static async ValueTask CreateNewAsync( var spreadsheet = new Spreadsheet(archive, compressionLevel, bufferSize, defaultDateTimeFormat, writeCellReferenceAttributes); await spreadsheet.InitializeAsync(cancellationToken).ConfigureAwait(false); - - // If no style is ever added to the spreadsheet, then we can skip creating the styles.xml file. - // If we have any style, the built-in default style must be the first one (meaning the first element in styles.xml). - if (defaultDateTimeFormat is not null) - spreadsheet.AddDefaultStyle(); - return spreadsheet; } @@ -387,11 +384,8 @@ public StyleId AddStyle(Style style) { ArgumentNullException.ThrowIfNull(style); - // If we have any style, the built-in default style must be the first one (meaning the first element in styles.xml). - if (_styleManager is null) - AddDefaultStyle(); - - return AddStyle(ImmutableStyle.From(style)); + var styleManager = _styleManager ?? new(defaultDateTimeFormat: null); + return styleManager.AddStyleIfNotExists(ImmutableStyle.From(style)); } public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleNameVisibility = null) @@ -409,7 +403,7 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName ThrowHelper.StyleNameCanNotEqualNormal(nameof(name)); // TODO: Test duplicate name but different casing - var styleManager = _styleManager ??= new(); + var styleManager = _styleManager ??= new(defaultDateTimeFormat: null); if (styleManager.StyleNameExists(name)) ThrowHelper.StyleNameAlreadyExists(nameof(name)); @@ -418,24 +412,11 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName // TODO: Maybe the named style should only refer to the regular style, not the DateTime style. var styleId = AddStyle(style); - // TODO: The styleManager can also handle the default style? styleManager.AddNamedStyle(name, styleId, styleNameVisibility); return styleId; } - private void AddDefaultStyle() - { - var styleManager = _styleManager ??= new(); - styleManager.AddDefaultStyle(_defaultDateTimeFormat); - } - - private StyleId AddStyle(in ImmutableStyle style) - { - _styleManager ??= new(); - return _styleManager.AddStyleIfNotExists(style, _defaultDateTimeFormat); - } - /// /// Get the from a named style. /// The named style must have previously been added to the spreadsheet with . diff --git a/SpreadCheetah/Styling/Internal/StyleManager.cs b/SpreadCheetah/Styling/Internal/StyleManager.cs index df78beb0..dc8d256b 100644 --- a/SpreadCheetah/Styling/Internal/StyleManager.cs +++ b/SpreadCheetah/Styling/Internal/StyleManager.cs @@ -9,20 +9,34 @@ private readonly record struct EmbeddedNamedStyle( StyleNameVisibility Visibility); private readonly Dictionary _styles = []; + private readonly NumberFormat? _defaultDateTimeFormat; private Dictionary? _namedStyles; private Dictionary? _embeddedNamedStyles; - public DefaultStyling? DefaultStyling { get; private set; } + public DefaultStyling? DefaultStyling { get; } - public StyleId AddStyleIfNotExists(in ImmutableStyle style, NumberFormat? defaultDateTimeFormat) + public StyleManager(NumberFormat? defaultDateTimeFormat) + { + _defaultDateTimeFormat = defaultDateTimeFormat; + + // If we have any style, the built-in default style must be the first one (meaning the first element in styles.xml). + var defaultFont = new ImmutableFont(null, false, false, false, Font.DefaultSize, null); + var defaultStyle = new ImmutableStyle(new ImmutableAlignment(), new ImmutableBorder(), new ImmutableFill(), defaultFont, null); + var styleId = AddStyleIfNotExists(defaultStyle); + + if (styleId.Id != styleId.DateTimeId) + DefaultStyling = new DefaultStyling(styleId.DateTimeId); + } + + public StyleId AddStyleIfNotExists(in ImmutableStyle style) { var id = AddStyleIfNotExistsInternal(style); - if (defaultDateTimeFormat is null || style.Format is not null) + if (_defaultDateTimeFormat is null || style.Format is not null) return new StyleId(id, id); // Optionally add another style for DateTime when there is no explicit number format in the new style. - var dateTimeStyle = style with { Format = defaultDateTimeFormat }; + var dateTimeStyle = style with { Format = _defaultDateTimeFormat }; var dateTimeId = AddStyleIfNotExistsInternal(dateTimeStyle); return new StyleId(id, dateTimeId); @@ -83,14 +97,4 @@ public List GetOrderedStyles() return result; } - - public void AddDefaultStyle(NumberFormat? defaultDateTimeFormat) - { - var defaultFont = new ImmutableFont(null, false, false, false, Font.DefaultSize, null); - var defaultStyle = new ImmutableStyle(new ImmutableAlignment(), new ImmutableBorder(), new ImmutableFill(), defaultFont, null); - var styleId = AddStyleIfNotExists(defaultStyle, defaultDateTimeFormat); - - if (styleId.Id != styleId.DateTimeId) - DefaultStyling = new DefaultStyling(styleId.DateTimeId); - } } From 4ffab095d0f28b777a184b1cbb25dcf25f86c16f Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 16:23:48 +0200 Subject: [PATCH 21/42] TryAddNamedStyle in StyleManager --- SpreadCheetah/Spreadsheet.cs | 11 ++------- .../Styling/Internal/StyleManager.cs | 23 +++++++++++++------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index f15408bf..bd01b95e 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -388,7 +388,7 @@ public StyleId AddStyle(Style style) return styleManager.AddStyleIfNotExists(ImmutableStyle.From(style)); } - public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleNameVisibility = null) + public StyleId AddStyle(Style style, string name, StyleNameVisibility? nameVisibility = null) { ArgumentNullException.ThrowIfNull(style); ArgumentNullException.ThrowIfNull(name); @@ -404,16 +404,9 @@ public StyleId AddStyle(Style style, string name, StyleNameVisibility? styleName // TODO: Test duplicate name but different casing var styleManager = _styleManager ??= new(defaultDateTimeFormat: null); - if (styleManager.StyleNameExists(name)) + if (!styleManager.TryAddNamedStyle(name, style, nameVisibility, out var styleId)) ThrowHelper.StyleNameAlreadyExists(nameof(name)); - // TODO: When there is a default DateTime number format, two style IDs will be created. - // TODO: How should this be handled for a named style? - // TODO: Maybe the named style should only refer to the regular style, not the DateTime style. - var styleId = AddStyle(style); - - styleManager.AddNamedStyle(name, styleId, styleNameVisibility); - return styleId; } diff --git a/SpreadCheetah/Styling/Internal/StyleManager.cs b/SpreadCheetah/Styling/Internal/StyleManager.cs index dc8d256b..8a863cc7 100644 --- a/SpreadCheetah/Styling/Internal/StyleManager.cs +++ b/SpreadCheetah/Styling/Internal/StyleManager.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace SpreadCheetah.Styling.Internal; internal sealed class StyleManager @@ -52,21 +54,28 @@ private int AddStyleIfNotExistsInternal(in ImmutableStyle style) return id; } - public bool StyleNameExists(string name) + public bool TryAddNamedStyle(string name, Style style, StyleNameVisibility? nameVisibility, + [NotNullWhen(true)] out StyleId? styleId) { - return _namedStyles is { } namedStyles && namedStyles.ContainsKey(name); - } + styleId = null; - public void AddNamedStyle(string name, StyleId styleId, StyleNameVisibility? visibility) - { var namedStyles = _namedStyles ??= new(StringComparer.OrdinalIgnoreCase); + if (namedStyles.ContainsKey(name)) + return false; + + // TODO: When there is a default DateTime number format, two style IDs will be created. + // TODO: How should this be handled for a named style? + // TODO: Maybe the named style should only refer to the regular style, not the DateTime style. + styleId = AddStyleIfNotExists(ImmutableStyle.From(style)); namedStyles[name] = styleId; - if (visibility is { } vis) + if (nameVisibility is { } visibility) { var embeddedNamedStyles = _embeddedNamedStyles ??= new(StringComparer.OrdinalIgnoreCase); - embeddedNamedStyles[name] = new EmbeddedNamedStyle(vis); + embeddedNamedStyles[name] = new EmbeddedNamedStyle(visibility); } + + return true; } public StyleId? GetStyleIdOrDefault(string name) => _namedStyles?.GetValueOrDefault(name); From e508af2c8700b2da43e1e8b7851919da01bb37ee Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 16:39:26 +0200 Subject: [PATCH 22/42] Add Name to StyleIdentifiers --- SpreadCheetah/MetadataXml/Styles/StylesXml.cs | 2 ++ SpreadCheetah/Spreadsheet.cs | 2 +- .../Styling/Internal/StyleManager.cs | 23 ++++++++++--------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs index 78c09eca..da5d7b02 100644 --- a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -216,7 +216,9 @@ private bool TryWriteCellXfsEntries() for (; _nextIndex < styles.Count; ++_nextIndex) { + // TODO: For cellXfs entries which are named styles, xfId should be the index into cellStyleXfs var style = _styles[_nextIndex]; + // TODO: Should pass xfId to this method somehow if (!xfXml.TryWrite(style)) return false; } diff --git a/SpreadCheetah/Spreadsheet.cs b/SpreadCheetah/Spreadsheet.cs index bd01b95e..bf1e05b9 100644 --- a/SpreadCheetah/Spreadsheet.cs +++ b/SpreadCheetah/Spreadsheet.cs @@ -385,7 +385,7 @@ public StyleId AddStyle(Style style) ArgumentNullException.ThrowIfNull(style); var styleManager = _styleManager ?? new(defaultDateTimeFormat: null); - return styleManager.AddStyleIfNotExists(ImmutableStyle.From(style)); + return styleManager.AddStyleIfNotExists(ImmutableStyle.From(style), null); } public StyleId AddStyle(Style style, string name, StyleNameVisibility? nameVisibility = null) diff --git a/SpreadCheetah/Styling/Internal/StyleManager.cs b/SpreadCheetah/Styling/Internal/StyleManager.cs index 8a863cc7..1c6284d0 100644 --- a/SpreadCheetah/Styling/Internal/StyleManager.cs +++ b/SpreadCheetah/Styling/Internal/StyleManager.cs @@ -5,7 +5,8 @@ namespace SpreadCheetah.Styling.Internal; internal sealed class StyleManager { private readonly record struct StyleIdentifiers( - int StyleId); + int StyleId, + string? Name); private readonly record struct EmbeddedNamedStyle( StyleNameVisibility Visibility); @@ -24,33 +25,36 @@ public StyleManager(NumberFormat? defaultDateTimeFormat) // If we have any style, the built-in default style must be the first one (meaning the first element in styles.xml). var defaultFont = new ImmutableFont(null, false, false, false, Font.DefaultSize, null); var defaultStyle = new ImmutableStyle(new ImmutableAlignment(), new ImmutableBorder(), new ImmutableFill(), defaultFont, null); - var styleId = AddStyleIfNotExists(defaultStyle); + var styleId = AddStyleIfNotExists(defaultStyle, null); if (styleId.Id != styleId.DateTimeId) DefaultStyling = new DefaultStyling(styleId.DateTimeId); } - public StyleId AddStyleIfNotExists(in ImmutableStyle style) + public StyleId AddStyleIfNotExists(in ImmutableStyle style, string? name) { - var id = AddStyleIfNotExistsInternal(style); + var id = AddStyleIfNotExistsInternal(style, name); if (_defaultDateTimeFormat is null || style.Format is not null) return new StyleId(id, id); // Optionally add another style for DateTime when there is no explicit number format in the new style. var dateTimeStyle = style with { Format = _defaultDateTimeFormat }; - var dateTimeId = AddStyleIfNotExistsInternal(dateTimeStyle); + + // If there is a name it will only refer to the regular style, not the DateTime style. + var dateTimeId = AddStyleIfNotExistsInternal(dateTimeStyle, null); return new StyleId(id, dateTimeId); } - private int AddStyleIfNotExistsInternal(in ImmutableStyle style) + private int AddStyleIfNotExistsInternal(in ImmutableStyle style, string? name) { + // TODO: What to do here if the style already exists, but with a different name? if (_styles.TryGetValue(style, out var existingIdentifiers)) return existingIdentifiers.StyleId; var id = _styles.Count; - _styles[style] = new StyleIdentifiers(id); + _styles[style] = new StyleIdentifiers(id, name); return id; } @@ -63,10 +67,7 @@ public bool TryAddNamedStyle(string name, Style style, StyleNameVisibility? name if (namedStyles.ContainsKey(name)) return false; - // TODO: When there is a default DateTime number format, two style IDs will be created. - // TODO: How should this be handled for a named style? - // TODO: Maybe the named style should only refer to the regular style, not the DateTime style. - styleId = AddStyleIfNotExists(ImmutableStyle.From(style)); + styleId = AddStyleIfNotExists(ImmutableStyle.From(style), name); namedStyles[name] = styleId; if (nameVisibility is { } visibility) From fff0ee2dcb5a5044732ef43f6bd95fd8ebd38440 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 16:44:12 +0200 Subject: [PATCH 23/42] Update public API snapshots after renaming parameter --- .../PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt | 2 +- .../PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt | 2 +- .../PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt | 2 +- .../Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt index baa3f57a..bcd3f900 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet6_0.verified.txt @@ -116,7 +116,7 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } - public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt index f4f1175d..96336d6b 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet7_0.verified.txt @@ -116,7 +116,7 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } - public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt index f7321583..07882bbe 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.DotNet8_0.verified.txt @@ -116,7 +116,7 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } - public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } diff --git a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt index ac3de460..d91077f9 100644 --- a/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt +++ b/SpreadCheetah.Test/Tests/PublicApiTests.PublicApi_Generate.Net4_7.verified.txt @@ -115,7 +115,7 @@ namespace SpreadCheetah public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public System.Threading.Tasks.ValueTask AddRowAsync(System.ReadOnlyMemory cells, SpreadCheetah.Worksheets.RowOptions? options, System.Threading.CancellationToken token = default) { } public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style) { } - public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? styleNameVisibility = default) { } + public SpreadCheetah.Styling.StyleId AddStyle(SpreadCheetah.Styling.Style style, string name, SpreadCheetah.Styling.StyleNameVisibility? nameVisibility = default) { } public void Dispose() { } public System.Threading.Tasks.ValueTask DisposeAsync() { } public System.Threading.Tasks.ValueTask EmbedImageAsync(System.IO.Stream stream, System.Threading.CancellationToken token = default) { } From 99f759d85110fb702bc541bbb49e402a9748a2a2 Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 21:24:02 +0200 Subject: [PATCH 24/42] Write named style index (xfId) for cellXfs entries --- SpreadCheetah/MetadataXml/Styles/StylesXml.cs | 55 ++++++++++++++----- SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs | 13 +++-- .../Styling/Internal/StyleManager.cs | 7 ++- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs index da5d7b02..1aec1b1c 100644 --- a/SpreadCheetah/MetadataXml/Styles/StylesXml.cs +++ b/SpreadCheetah/MetadataXml/Styles/StylesXml.cs @@ -43,9 +43,10 @@ public static async ValueTask WriteAsync( """"""u8 + """"""u8; - private readonly List _styles; + private readonly List<(ImmutableStyle Style, string? EmbeddedName)> _styles; private readonly List<(string, ImmutableStyle, StyleNameVisibility)>? _namedStyles; private readonly Dictionary? _customNumberFormats; + private readonly Dictionary? _embeddedNamedStyleIndexes; private readonly Dictionary _borders; private readonly Dictionary _fills; private readonly Dictionary _fonts; @@ -59,11 +60,12 @@ public static async ValueTask WriteAsync( private int _nextIndex; private StylesXml( - List styles, + List<(ImmutableStyle Style, string? EmbeddedName)> styles, List<(string, ImmutableStyle, StyleNameVisibility)>? namedStyles, SpreadsheetBuffer buffer) { _customNumberFormats = CreateCustomNumberFormatDictionary(styles); + _embeddedNamedStyleIndexes = CreateEmbeddedStyleNameIndexesDictionary(styles, namedStyles); _borders = CreateBorderDictionary(styles); _fills = CreateFillDictionary(styles); _fonts = CreateFontDictionary(styles); @@ -80,12 +82,12 @@ private StylesXml( public readonly StylesXml GetEnumerator() => this; public bool Current { get; private set; } - private static Dictionary? CreateCustomNumberFormatDictionary(List styles) + private static Dictionary? CreateCustomNumberFormatDictionary(List<(ImmutableStyle Style, string? EmbeddedName)> styles) { Dictionary? dictionary = null; var numberFormatId = 165; // Custom formats start sequentially from this ID - foreach (var style in styles) + foreach (var (style, _) in styles) { var numberFormat = style.Format; if (numberFormat is not { } format) continue; @@ -99,7 +101,7 @@ private StylesXml( return dictionary; } - private static Dictionary CreateBorderDictionary(List styles) + private static Dictionary CreateBorderDictionary(List<(ImmutableStyle Style, string? EmbeddedName)> styles) { var defaultBorder = new ImmutableBorder(); const int defaultCount = 1; @@ -107,7 +109,7 @@ private static Dictionary CreateBorderDictionary(List { { defaultBorder, 0 } }; var borderIndex = defaultCount; - foreach (var style in styles) + foreach (var (style, _) in styles) { if (uniqueBorders.TryAdd(style.Border, borderIndex)) ++borderIndex; @@ -116,7 +118,7 @@ private static Dictionary CreateBorderDictionary(List CreateFillDictionary(List styles) + private static Dictionary CreateFillDictionary(List<(ImmutableStyle Style, string? EmbeddedName)> styles) { var defaultFill = new ImmutableFill(); const int defaultCount = 2; @@ -124,7 +126,7 @@ private static Dictionary CreateFillDictionary(List { { defaultFill, 0 } }; var fillIndex = defaultCount; - foreach (var style in styles) + foreach (var (style, _) in styles) { if (uniqueFills.TryAdd(style.Fill, fillIndex)) ++fillIndex; @@ -133,7 +135,7 @@ private static Dictionary CreateFillDictionary(List CreateFontDictionary(List styles) + private static Dictionary CreateFontDictionary(List<(ImmutableStyle Style, string? EmbeddedName)> styles) { var defaultFont = new ImmutableFont { Size = Font.DefaultSize }; const int defaultCount = 1; @@ -141,7 +143,7 @@ private static Dictionary CreateFontDictionary(List { { defaultFont, 0 } }; var fontIndex = defaultCount; - foreach (var style in styles) + foreach (var (style, _) in styles) { if (uniqueFonts.TryAdd(style.Font, fontIndex)) ++fontIndex; @@ -150,6 +152,27 @@ private static Dictionary CreateFontDictionary(List? CreateEmbeddedStyleNameIndexesDictionary( + List<(ImmutableStyle Style, string? EmbeddedName)> styles, + List<(string, ImmutableStyle, StyleNameVisibility)>? namedStyles) + { + if (namedStyles is null) + return null; + + Dictionary? result = null; + + foreach (var (_, name) in styles) + { + if (name is null) + continue; + + result ??= new(StringComparer.OrdinalIgnoreCase); + result[name] = namedStyles.FindIndex(x => name.Equals(x.Item1, StringComparison.OrdinalIgnoreCase)); + } + + return result; + } + public bool MoveNext() { Current = _next switch @@ -193,7 +216,7 @@ private bool TryWriteCellStyleXfsEntries() for (; _nextIndex < _namedStyles.Count; ++_nextIndex) { var (_, style, _) = namedStyles[_nextIndex]; - if (!xfXml.TryWrite(style)) + if (!xfXml.TryWrite(style, null)) return false; } @@ -216,10 +239,12 @@ private bool TryWriteCellXfsEntries() for (; _nextIndex < styles.Count; ++_nextIndex) { - // TODO: For cellXfs entries which are named styles, xfId should be the index into cellStyleXfs - var style = _styles[_nextIndex]; - // TODO: Should pass xfId to this method somehow - if (!xfXml.TryWrite(style)) + var (style, embeddedName) = _styles[_nextIndex]; + var embeddedStyleIndex = embeddedName is not null + ? _embeddedNamedStyleIndexes?.GetValueOrDefault(embeddedName) + : null; + + if (!xfXml.TryWrite(style, embeddedStyleIndex)) return false; } diff --git a/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs index 2bb3a0e7..361c96a8 100644 --- a/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs +++ b/SpreadCheetah/MetadataXml/Styles/XfXmlPart.cs @@ -12,7 +12,7 @@ internal readonly struct XfXmlPart( Dictionary fonts, bool cellXfsEntry) { - public bool TryWrite(in ImmutableStyle style) + public bool TryWrite(in ImmutableStyle style, int? embeddedNamedStyleIndex) { var span = buffer.GetSpan(); var written = 0; @@ -34,11 +34,12 @@ public bool TryWrite(in ImmutableStyle style) if (!"\" applyBorder=\"1\""u8.TryCopyTo(span, ref written)) return false; } - // TODO: For cellXfs entries which are normal styles, xfId should be 0 - // TODO: For cellXfs entires which are named styles, xfId should be the index into cellStyleXfs - // TODO: -> What about DateTime cells with a default style? - // TODO: For cellStyleXfs entries, there should not be xfId - if (cellXfsEntry && !" xfId=\"0\""u8.TryCopyTo(span, ref written)) return false; + if (cellXfsEntry) + { + if (!" xfId=\""u8.TryCopyTo(span, ref written)) return false; + if (!SpanHelper.TryWrite(embeddedNamedStyleIndex ?? 0, span, ref written)) return false; + if (!"\""u8.TryCopyTo(span, ref written)) return false; + } if (style.Alignment == default) { diff --git a/SpreadCheetah/Styling/Internal/StyleManager.cs b/SpreadCheetah/Styling/Internal/StyleManager.cs index 1c6284d0..33ce0b91 100644 --- a/SpreadCheetah/Styling/Internal/StyleManager.cs +++ b/SpreadCheetah/Styling/Internal/StyleManager.cs @@ -81,10 +81,13 @@ public bool TryAddNamedStyle(string name, Style style, StyleNameVisibility? name public StyleId? GetStyleIdOrDefault(string name) => _namedStyles?.GetValueOrDefault(name); - public List GetOrderedStyles() + public List<(ImmutableStyle Style, string? EmbeddedName)> GetOrderedStyles() { // The order of Dictionary.Keys is not guaranteed, so we make sure the styles are sorted by the StyleId here. - return _styles.OrderBy(x => x.Value.StyleId).Select(x => x.Key).ToList(); + return _styles + .OrderBy(x => x.Value.StyleId) + .Select(x => (x.Key, x.Value.Name)) + .ToList(); } public List<(string, ImmutableStyle, StyleNameVisibility)>? GetEmbeddedNamedStyles() From 95a6f7aebdbe1195e5c711888a33c921a5de9b1e Mon Sep 17 00:00:00 2001 From: sveinungf Date: Sat, 10 Aug 2024 22:50:37 +0200 Subject: [PATCH 25/42] Fix missing equal sign for hidden named cell styles --- SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs index dae5c469..1d988178 100644 --- a/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs +++ b/SpreadCheetah/MetadataXml/Styles/CellStylesXmlPart.cs @@ -68,7 +68,7 @@ private bool TryWriteCellStyles() { if (!"