Skip to content

Commit

Permalink
Store rich text as an immutable rich text through a new type XLImmuta…
Browse files Browse the repository at this point in the history
…bleRichText. The XLRichText transparently updates immutable rich text, whenever it changes (run, pohonetic runs or phonetic properties). This is required for shared string table, because items stored in a reverse dictionary must be immutable.
  • Loading branch information
jahav committed Jul 2, 2023
1 parent 8043236 commit 1e147d0
Show file tree
Hide file tree
Showing 20 changed files with 834 additions and 188 deletions.
47 changes: 47 additions & 0 deletions ClosedXML.Tests/Excel/RichText/XLImmutableRichTextTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Linq;
using ClosedXML.Excel;
using NUnit.Framework;

namespace ClosedXML.Tests.Excel.RichText
{
[TestFixture]
public class XLImmutableRichTextTests
{
[Test]
public void Equals_compares_text_runs_phonetic_runs_and_properties()
{
using var wb = new XLWorkbook();
var ws = wb.AddWorksheet();
var richText = (XLRichText)ws.Cell("A1").CreateRichText();
richText
.AddText("こんにち").SetBold(true) // Hello in hiragana
.AddText("は,").SetBold(false) // object marker
.AddText("世界").SetFontSize(15); // world in kanji
richText.Phonetics
.SetAlignment(XLPhoneticAlignment.Distributed)
.Add(@"konnichi wa", 0, 6); // world in hiragana

// Assert equal
var immutableRichText = new XLImmutableRichText(richText);
var equalImmutableRichText = new XLImmutableRichText(richText);
Assert.AreEqual(immutableRichText, equalImmutableRichText);

// Different font of a first run
richText.ElementAt(0).SetBold(false);
var withDifferentTextRunFont = new XLImmutableRichText(richText);
Assert.AreNotEqual(immutableRichText, withDifferentTextRunFont);
richText.ElementAt(0).SetBold(true);

// Different phonetic properties
richText.Phonetics.SetAlignment(XLPhoneticAlignment.Left);
var withDifferentPhoneticsProps = new XLImmutableRichText(richText);
Assert.AreNotEqual(immutableRichText, withDifferentPhoneticsProps);
richText.Phonetics.SetAlignment(XLPhoneticAlignment.Distributed);

// Different phonetic runs
richText.Phonetics.Add("せかい", 6, 8);
var withDifferentTextPhonetics = new XLImmutableRichText(richText);
Assert.AreNotEqual(immutableRichText, withDifferentTextPhonetics);
}
}
}
1 change: 1 addition & 0 deletions ClosedXML.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=MMULT/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=sparklines/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=SUMIF/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tahoma/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Unconvertable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=underlaying/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Upscaled/@EntryIndexedValue">True</s:Boolean>
Expand Down
10 changes: 5 additions & 5 deletions ClosedXML/Excel/Cells/ValueSlice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,15 @@ internal void SetCellValue(XLSheetPoint point, XLCellValue cellValue)
_values.Set(point, in modified);
}

internal XLRichText GetRichText(XLSheetPoint point)
internal XLImmutableRichText? GetRichText(XLSheetPoint point)
{
return _values[point].RichText;
}

internal void SetRichText(XLSheetPoint point, XLRichText richText)
internal void SetRichText(XLSheetPoint point, XLImmutableRichText? richText)
{
ref readonly var original = ref _values[point];
if (original.RichText != richText)
if (!ReferenceEquals(original.RichText, richText))
{
var modified = new XLValueSliceContent(original.Value, original.Type, original.ModifiedAtVersion, original.SharedStringId, richText);
_values.Set(point, in modified);
Expand Down Expand Up @@ -220,9 +220,9 @@ private void DereferenceTextInRange(XLSheetRange range)
internal readonly XLDataType Type;
internal readonly long ModifiedAtVersion;
internal readonly int SharedStringId;
internal readonly XLRichText RichText;
internal readonly XLImmutableRichText? RichText;

internal XLValueSliceContent(double value, XLDataType type, long modifiedAtVersion, int sharedStringId, XLRichText richText)
internal XLValueSliceContent(double value, XLDataType type, long modifiedAtVersion, int sharedStringId, XLImmutableRichText? richText)
{
Value = value;
Type = type;
Expand Down
39 changes: 22 additions & 17 deletions ClosedXML/Excel/Cells/XLCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private XLCellValue SliceCellValue
set => _cellsCollection.ValueSlice.SetCellValue(SheetPoint, value);
}

private XLRichText SliceRichText
private XLImmutableRichText SliceRichText
{
get => _cellsCollection.ValueSlice.GetRichText(SheetPoint);
set => _cellsCollection.ValueSlice.SetRichText(SheetPoint, value);
Expand Down Expand Up @@ -257,17 +257,24 @@ internal XLComment CreateComment(int? shapeId = null)

public XLRichText GetRichText()
{
return SliceRichText ?? CreateRichText();
var sliceRichText = SliceRichText;
if (sliceRichText is not null)
return new XLRichText(this, sliceRichText);

return CreateRichText();
}

public XLRichText CreateRichText()
{
var style = GetStyleForRead();
SliceRichText = Value.Type == XLDataType.Blank
? new XLRichText(this, new XLFont(Style as XLStyle, style.Font))
: new XLRichText(this, GetFormattedString(), new XLFont(Style as XLStyle, style.Font));
SliceCellValue = SliceRichText.Text;
return SliceRichText;
var font = new XLFont(GetStyleForRead().Font.Key);

// Don't include rich text string with 0 length to a new rich text
var richText = DataType == XLDataType.Blank
? new XLRichText(this, font)
: new XLRichText(this, GetFormattedString(), font);
SliceRichText = new XLImmutableRichText(richText);
SliceCellValue = richText.Text;
return richText;
}

#region IXLCell Members
Expand Down Expand Up @@ -1079,10 +1086,7 @@ public bool NeedsRecalculation

IXLRichText IXLCell.GetRichText() => GetRichText();

public bool HasRichText
{
get { return SliceRichText != null; }
}
public bool HasRichText => SliceRichText is not null;

IXLRichText IXLCell.CreateRichText() => CreateRichText();

Expand Down Expand Up @@ -1121,7 +1125,7 @@ public Boolean IsEmpty(XLCellsUsedOptions options)
{
bool isValueEmpty;
if (HasRichText)
isValueEmpty = SliceRichText.Length == 0;
isValueEmpty = SliceRichText.Text.Length == 0;
else
{
isValueEmpty = SliceCellValue.Type switch
Expand Down Expand Up @@ -1587,7 +1591,7 @@ internal void CopyValuesFrom(XLCell source)
{
SliceCellValue = source.SliceCellValue;
FormulaR1C1 = source.FormulaR1C1;
SliceRichText = source.SliceRichText == null ? null : new XLRichText(this, source.SliceRichText, source.Style.Font);
SliceRichText = source.SliceRichText;
SliceComment = source.SliceComment == null ? null : new XLComment(this, source.SliceComment, source.Style.Font, source.SliceComment.Style);
if (source.SliceHyperlink != null)
{
Expand Down Expand Up @@ -2246,10 +2250,11 @@ internal void GetGlyphBoxes(IXLGraphicEngine engine, Dpi dpi, List<GlyphBox> out
var richText = SliceRichText;
if (richText is not null)
{
foreach (var richString in richText)
foreach (var richTextRun in richText.Runs)
{
IXLFontBase font = richString;
AddGlyphs(richString.Text, font, engine, dpi, output);
var text = richText.GetRunText(richTextRun);
var font = new XLFont(richTextRun.Font.Key);
AddGlyphs(text, font, engine, dpi, output);
}
}
else
Expand Down
2 changes: 1 addition & 1 deletion ClosedXML/Excel/Cells/XLCellsCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal HashSet<int> RowsUsedKeys

internal Slice<XLCellFormula> FormulaSlice { get; } = new();

internal Slice<XLStyleValue> StyleSlice { get; } = new();
internal Slice<XLStyleValue?> StyleSlice { get; } = new();

internal Slice<XLMiscSliceContent> MiscSlice { get; } = new();

Expand Down
4 changes: 2 additions & 2 deletions ClosedXML/Excel/Cells/XLMiscSliceContent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ internal bool ShareString
set => _inlineString = !value;
}

internal XLComment Comment { get; set; }
internal XLComment? Comment { get; set; }

internal XLHyperlink Hyperlink { get; set; }
internal XLHyperlink? Hyperlink { get; set; }

internal uint? CellMetaIndex { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion ClosedXML/Excel/PageSetup/XLHFItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public IXLRichString AddText(XLHFPredefinedText predefinedText)

public IXLRichString AddText(String text, XLHFOccurrence occurrence)
{
XLRichString richText = new XLRichString(text, this.HeaderFooter.Worksheet.Style.Font, this);
var richText = new XLRichString(text, HeaderFooter.Worksheet.Style.Font, this, null);

var hfText = new XLHFText(richText, this);
if (occurrence == XLHFOccurrence.AllPages)
Expand Down
23 changes: 21 additions & 2 deletions ClosedXML/Excel/RichText/IXLFormattedText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,36 @@ public interface IXLFormattedText<T> : IEnumerable<IXLRichString>, IEquatable<IX
IXLFormattedText<T> Substring(Int32 index, Int32 length);

/// <summary>
/// Copy the text and formatting from the original text.
/// Replace the text and formatting of this text by texts and formatting from the <paramref name="original"/> text.
/// </summary>
/// <param name="original">Original to copy from.</param>
/// <returns>This text.</returns>
IXLFormattedText<T> CopyFrom(IXLFormattedText<T> original);

/// <summary>
/// How many rich strings is the formatted text composed of.
/// </summary>
Int32 Count { get; }

/// <summary>
/// Length of the whole formatted text.
/// </summary>
Int32 Length { get; }

/// <summary>
/// Get text of the whole formatted text.
/// </summary>
String Text { get; }
IXLPhonetics Phonetics { get; }

/// <summary>
/// Does this text has phonetics? Unlike accessing the <see cref="Phonetics"/> property, this method
/// doesn't create a new instance on access.
/// </summary>
Boolean HasPhonetics { get; }

/// <summary>
/// Get or create phonetics for the text. Use <see cref="HasPhonetics"/> to check for existence to avoid unnecessary creation.
/// </summary>
IXLPhonetics Phonetics { get; }
}
}
8 changes: 3 additions & 5 deletions ClosedXML/Excel/RichText/IXLPhonetic.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
#nullable disable

using System;

namespace ClosedXML.Excel
{
public interface IXLPhonetic: IEquatable<IXLPhonetic>
{
String Text { get; set; }
Int32 Start { get; set; }
Int32 End { get; set; }
String Text { get; }
Int32 Start { get; }
Int32 End { get; }
}
}
21 changes: 19 additions & 2 deletions ClosedXML/Excel/RichText/IXLPhonetics.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#nullable disable

using System;
using System.Collections.Generic;

Expand All @@ -20,10 +18,29 @@ public interface IXLPhonetics : IXLFontBase, IEnumerable<IXLPhonetic>, IEquatabl
IXLPhonetics SetFontName(String value);
IXLPhonetics SetFontFamilyNumbering(XLFontFamilyNumberingValues value);
IXLPhonetics SetFontCharSet(XLFontCharSet value);
IXLPhonetics SetFontScheme(XLFontScheme value);

/// <summary>
/// Add a phonetic run above a base text. Phonetic runs can't overlap.
/// </summary>
/// <param name="text">Text to display above a section of a base text. Can't be empty.</param>
/// <param name="start">Index of a first character of a base text above which should <paramref name="text"/> be displayed. Valid values are <c>0</c>..<c>length-1</c>.</param>
/// <param name="end">The excluded ending index in a base text (the hint is not displayed above the <c>end</c>). Must be &gt; <paramref name="start"/>. Valid values are <c>1</c>..<c>length</c>.</param>
IXLPhonetics Add(String text, Int32 start, Int32 end);

/// <summary>
/// Remove all phonetic runs. Keeps font properties.
/// </summary>
IXLPhonetics ClearText();

/// <summary>
/// Reset font properties to the default font of a container (likely <c>IXLCell</c>). Keeps phonetic runs, <see cref="Type"/> and <see cref="Alignment"/>.
/// </summary>
IXLPhonetics ClearFont();

/// <summary>
/// Number of phonetic runs above the base text.
/// </summary>
Int32 Count { get; }

XLPhoneticAlignment Alignment { get; set; }
Expand Down
2 changes: 0 additions & 2 deletions ClosedXML/Excel/RichText/IXLRichText.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#nullable disable

namespace ClosedXML.Excel
{
public interface IXLRichText : IXLFormattedText<IXLRichText>
Expand Down

0 comments on commit 1e147d0

Please sign in to comment.