diff --git a/ClosedXML.Tests/Excel/RichText/XLImmutableRichTextTests.cs b/ClosedXML.Tests/Excel/RichText/XLImmutableRichTextTests.cs
new file mode 100644
index 000000000..c0a4679db
--- /dev/null
+++ b/ClosedXML.Tests/Excel/RichText/XLImmutableRichTextTests.cs
@@ -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);
+ }
+ }
+}
diff --git a/ClosedXML.sln.DotSettings b/ClosedXML.sln.DotSettings
index e64c6c3a4..bdcc36582 100644
--- a/ClosedXML.sln.DotSettings
+++ b/ClosedXML.sln.DotSettings
@@ -23,6 +23,7 @@
True
True
True
+ True
True
True
True
diff --git a/ClosedXML/Excel/Cells/ValueSlice.cs b/ClosedXML/Excel/Cells/ValueSlice.cs
index 369753e0f..35054e9cc 100644
--- a/ClosedXML/Excel/Cells/ValueSlice.cs
+++ b/ClosedXML/Excel/Cells/ValueSlice.cs
@@ -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);
@@ -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;
diff --git a/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/Excel/Cells/XLCell.cs
index c5cf48371..cc8c766d8 100644
--- a/ClosedXML/Excel/Cells/XLCell.cs
+++ b/ClosedXML/Excel/Cells/XLCell.cs
@@ -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);
@@ -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
@@ -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();
@@ -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
@@ -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)
{
@@ -2246,10 +2250,11 @@ internal void GetGlyphBoxes(IXLGraphicEngine engine, Dpi dpi, List 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
diff --git a/ClosedXML/Excel/Cells/XLCellsCollection.cs b/ClosedXML/Excel/Cells/XLCellsCollection.cs
index 29f19c77e..841e422ed 100644
--- a/ClosedXML/Excel/Cells/XLCellsCollection.cs
+++ b/ClosedXML/Excel/Cells/XLCellsCollection.cs
@@ -70,7 +70,7 @@ internal HashSet RowsUsedKeys
internal Slice FormulaSlice { get; } = new();
- internal Slice StyleSlice { get; } = new();
+ internal Slice StyleSlice { get; } = new();
internal Slice MiscSlice { get; } = new();
diff --git a/ClosedXML/Excel/Cells/XLMiscSliceContent.cs b/ClosedXML/Excel/Cells/XLMiscSliceContent.cs
index 4d6a80041..41380e0b6 100644
--- a/ClosedXML/Excel/Cells/XLMiscSliceContent.cs
+++ b/ClosedXML/Excel/Cells/XLMiscSliceContent.cs
@@ -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; }
diff --git a/ClosedXML/Excel/PageSetup/XLHFItem.cs b/ClosedXML/Excel/PageSetup/XLHFItem.cs
index 87eee1a51..b8bf4a286 100644
--- a/ClosedXML/Excel/PageSetup/XLHFItem.cs
+++ b/ClosedXML/Excel/PageSetup/XLHFItem.cs
@@ -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)
diff --git a/ClosedXML/Excel/RichText/IXLFormattedText.cs b/ClosedXML/Excel/RichText/IXLFormattedText.cs
index 085210b27..f64edfc6a 100644
--- a/ClosedXML/Excel/RichText/IXLFormattedText.cs
+++ b/ClosedXML/Excel/RichText/IXLFormattedText.cs
@@ -36,17 +36,36 @@ public interface IXLFormattedText : IEnumerable, IEquatable Substring(Int32 index, Int32 length);
///
- /// Copy the text and formatting from the original text.
+ /// Replace the text and formatting of this text by texts and formatting from the text.
///
/// Original to copy from.
/// This text.
IXLFormattedText CopyFrom(IXLFormattedText original);
+ ///
+ /// How many rich strings is the formatted text composed of.
+ ///
Int32 Count { get; }
+
+ ///
+ /// Length of the whole formatted text.
+ ///
Int32 Length { get; }
+ ///
+ /// Get text of the whole formatted text.
+ ///
String Text { get; }
- IXLPhonetics Phonetics { get; }
+
+ ///
+ /// Does this text has phonetics? Unlike accessing the property, this method
+ /// doesn't create a new instance on access.
+ ///
Boolean HasPhonetics { get; }
+
+ ///
+ /// Get or create phonetics for the text. Use to check for existence to avoid unnecessary creation.
+ ///
+ IXLPhonetics Phonetics { get; }
}
}
diff --git a/ClosedXML/Excel/RichText/IXLPhonetic.cs b/ClosedXML/Excel/RichText/IXLPhonetic.cs
index 118147ebf..a7559a534 100644
--- a/ClosedXML/Excel/RichText/IXLPhonetic.cs
+++ b/ClosedXML/Excel/RichText/IXLPhonetic.cs
@@ -1,13 +1,11 @@
-#nullable disable
-
using System;
namespace ClosedXML.Excel
{
public interface IXLPhonetic: IEquatable
{
- String Text { get; set; }
- Int32 Start { get; set; }
- Int32 End { get; set; }
+ String Text { get; }
+ Int32 Start { get; }
+ Int32 End { get; }
}
}
diff --git a/ClosedXML/Excel/RichText/IXLPhonetics.cs b/ClosedXML/Excel/RichText/IXLPhonetics.cs
index 3f648063b..5f2ee0816 100644
--- a/ClosedXML/Excel/RichText/IXLPhonetics.cs
+++ b/ClosedXML/Excel/RichText/IXLPhonetics.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
@@ -20,10 +18,29 @@ public interface IXLPhonetics : IXLFontBase, IEnumerable, IEquatabl
IXLPhonetics SetFontName(String value);
IXLPhonetics SetFontFamilyNumbering(XLFontFamilyNumberingValues value);
IXLPhonetics SetFontCharSet(XLFontCharSet value);
+ IXLPhonetics SetFontScheme(XLFontScheme value);
+ ///
+ /// Add a phonetic run above a base text. Phonetic runs can't overlap.
+ ///
+ /// Text to display above a section of a base text. Can't be empty.
+ /// Index of a first character of a base text above which should be displayed. Valid values are 0..length-1.
+ /// The excluded ending index in a base text (the hint is not displayed above the end). Must be > . Valid values are 1..length.
IXLPhonetics Add(String text, Int32 start, Int32 end);
+
+ ///
+ /// Remove all phonetic runs. Keeps font properties.
+ ///
IXLPhonetics ClearText();
+
+ ///
+ /// Reset font properties to the default font of a container (likely IXLCell). Keeps phonetic runs, and .
+ ///
IXLPhonetics ClearFont();
+
+ ///
+ /// Number of phonetic runs above the base text.
+ ///
Int32 Count { get; }
XLPhoneticAlignment Alignment { get; set; }
diff --git a/ClosedXML/Excel/RichText/IXLRichText.cs b/ClosedXML/Excel/RichText/IXLRichText.cs
index 9965cf67e..f1fbaede6 100644
--- a/ClosedXML/Excel/RichText/IXLRichText.cs
+++ b/ClosedXML/Excel/RichText/IXLRichText.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
namespace ClosedXML.Excel
{
public interface IXLRichText : IXLFormattedText
diff --git a/ClosedXML/Excel/RichText/XLFormattedText.cs b/ClosedXML/Excel/RichText/XLFormattedText.cs
index e71027c37..4d7035618 100644
--- a/ClosedXML/Excel/RichText/XLFormattedText.cs
+++ b/ClosedXML/Excel/RichText/XLFormattedText.cs
@@ -9,17 +9,14 @@ namespace ClosedXML.Excel
{
internal class XLFormattedText : IXLFormattedText
{
+ ///
+ /// Font used for a new rich text run, never modified. It is generally provided by a container of the formatted text.
+ ///
private readonly IXLFontBase _defaultFont;
private List _richTexts = new();
- protected event EventHandler ContentChanged;
+ private XLPhonetics _phonetics;
protected T Container;
- public XLFormattedText(IXLFontBase defaultFont)
- {
- Length = 0;
- _defaultFont = defaultFont;
- }
-
public XLFormattedText(IXLFormattedText defaultRichText, IXLFontBase defaultFont)
: this(defaultFont)
{
@@ -27,7 +24,7 @@ public XLFormattedText(IXLFormattedText defaultRichText, IXLFontBase defaultF
AddText(rt.Text, rt);
if (defaultRichText.HasPhonetics)
{
- _phonetics = new XLPhonetics(defaultRichText.Phonetics, defaultFont);
+ _phonetics = new XLPhonetics(defaultRichText.Phonetics, defaultFont, OnContentChanged);
}
}
@@ -37,16 +34,33 @@ public XLFormattedText(String text, IXLFontBase defaultFont)
AddText(text);
}
- public Int32 Count { get { return _richTexts.Count; } }
+ public XLFormattedText(IXLFontBase defaultFont)
+ {
+ Length = 0;
+ _defaultFont = defaultFont;
+ }
+
+ IXLPhonetics IXLFormattedText.Phonetics => Phonetics;
+
+ public Int32 Count => _richTexts.Count;
+
public int Length { get; private set; }
+ public String Text => ToString();
+
+ public Boolean HasPhonetics => _phonetics is not null;
+
+ ///
+ internal XLPhonetics Phonetics => _phonetics ??= new XLPhonetics(_defaultFont, OnContentChanged);
+
public IXLRichString AddText(String text)
{
return AddText(text, _defaultFont);
}
+
public IXLRichString AddText(String text, IXLFontBase font)
{
- var richText = new XLRichString(text, font, this);
+ var richText = new XLRichString(text, font, this, OnContentChanged);
return AddText(richText);
}
@@ -54,7 +68,7 @@ public IXLRichString AddText(XLRichString richText)
{
_richTexts.Add(richText);
Length += richText.Text.Length;
- ContentChanged?.Invoke(this, EventArgs.Empty);
+ OnContentChanged();
return richText;
}
@@ -67,7 +81,7 @@ public IXLFormattedText ClearText()
{
_richTexts.Clear();
Length = 0;
- ContentChanged?.Invoke(this, EventArgs.Empty);
+ OnContentChanged();
return this;
}
@@ -111,7 +125,7 @@ public IXLFormattedText Substring(Int32 index, Int32 length)
Int32 startIndex = index - lastPosition;
if (startIndex > 0)
- newRichTexts.Add(new XLRichString(rt.Text.Substring(0, startIndex), rt, this));
+ newRichTexts.Add(new XLRichString(rt.Text.Substring(0, startIndex), rt, this, OnContentChanged));
else if (startIndex < 0)
startIndex = 0;
@@ -119,12 +133,12 @@ public IXLFormattedText Substring(Int32 index, Int32 length)
if (leftToTake > rt.Text.Length - startIndex)
leftToTake = rt.Text.Length - startIndex;
- XLRichString newRt = new XLRichString(rt.Text.Substring(startIndex, leftToTake), rt, this);
+ var newRt = new XLRichString(rt.Text.Substring(startIndex, leftToTake), rt, this, OnContentChanged);
newRichTexts.Add(newRt);
retVal.AddText(newRt);
if (startIndex + leftToTake < rt.Text.Length)
- newRichTexts.Add(new XLRichString(rt.Text.Substring(startIndex + leftToTake), rt, this));
+ newRichTexts.Add(new XLRichString(rt.Text.Substring(startIndex + leftToTake), rt, this, OnContentChanged));
}
else // We haven't reached the desired position yet
{
@@ -133,7 +147,7 @@ public IXLFormattedText Substring(Int32 index, Int32 length)
lastPosition += rt.Text.Length;
}
_richTexts = newRichTexts;
- ContentChanged?.Invoke(this, EventArgs.Empty);
+ OnContentChanged();
return retVal;
}
@@ -141,7 +155,7 @@ public IXLFormattedText CopyFrom(IXLFormattedText original)
{
ClearText();
foreach (var richText in original)
- AddText(new XLRichString(richText.Text, richText, this));
+ AddText(new XLRichString(richText.Text, richText, this, OnContentChanged));
return this;
}
@@ -181,27 +195,27 @@ public IXLFormattedText CopyFrom(IXLFormattedText original)
public bool Equals(IXLFormattedText other)
{
- Int32 count = Count;
- if (count != other.Count)
+ if (other is null)
return false;
- for (Int32 i = 0; i < count; i++)
- {
- if (_richTexts.ElementAt(i) != other.ElementAt(i))
- return false;
- }
+ if (ReferenceEquals(this, other))
+ return true;
- return _phonetics == null || Phonetics.Equals(other.Phonetics);
- }
+ if (Count != other.Count)
+ return false;
+
+ if (!_richTexts.SequenceEqual(other))
+ return false;
- public String Text { get { return ToString(); } }
+ return (_phonetics is null && !other.HasPhonetics) || Phonetics.Equals(other.Phonetics);
+ }
- private IXLPhonetics _phonetics;
- public IXLPhonetics Phonetics
+ ///
+ /// This method is called every time the formatted text is changed (new runs, font props, phonetics...).
+ ///
+ protected virtual void OnContentChanged()
{
- get { return _phonetics ?? (_phonetics = new XLPhonetics(_defaultFont)); }
+ // Do nothing, intended to be overriden.
}
-
- public Boolean HasPhonetics { get { return _phonetics != null; } }
}
}
diff --git a/ClosedXML/Excel/RichText/XLImmutableRichText.cs b/ClosedXML/Excel/RichText/XLImmutableRichText.cs
new file mode 100644
index 000000000..be08ca723
--- /dev/null
+++ b/ClosedXML/Excel/RichText/XLImmutableRichText.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace ClosedXML.Excel
+{
+ ///
+ /// A class for holding in a .
+ /// It's immutable (keys in reverse dictionary can't change) and more memory efficient
+ /// than mutable rich text.
+ ///
+ [DebuggerDisplay("{Text}")]
+ internal sealed class XLImmutableRichText : IEquatable
+ {
+ private readonly RichTextRun[] _runs;
+ private readonly PhoneticRun[] _phoneticRuns;
+
+ ///
+ /// Create an immutable rich text with same content as the original .
+ ///
+ internal XLImmutableRichText(XLRichText richText)
+ {
+ Text = richText.Text;
+ _runs = new RichTextRun[richText.Count];
+ var runIdx = 0;
+ var charStartIdx = 0;
+ foreach (var richString in richText)
+ {
+ _runs[runIdx++] = new RichTextRun(richString, charStartIdx, richString.Text.Length);
+ charStartIdx += richString.Text.Length;
+ }
+
+ if (richText.HasPhonetics)
+ {
+ var rtPhonetics = richText.Phonetics;
+ _phoneticRuns = new PhoneticRun[rtPhonetics.Count];
+ var phoneticRunIdx = 0;
+ var prevPhoneticEndIdx = 0;
+ foreach (var phonetic in richText.Phonetics)
+ {
+ if (phonetic.Start >= Text.Length)
+ throw new ArgumentException("Phonetic run start index must be within the text boundaries.");
+
+ if (phonetic.End > Text.Length)
+ throw new ArgumentException("Phonetic run end index must be at most length of a text.");
+
+ if (phonetic.Start < prevPhoneticEndIdx)
+ throw new ArgumentException("Phonetic runs must be in ascending order and can't overlap.");
+
+ _phoneticRuns[phoneticRunIdx++] = new PhoneticRun(phonetic.Text, phonetic.Start, phonetic.End);
+ prevPhoneticEndIdx = phonetic.End;
+ }
+
+ PhoneticsProperties = new PhoneticProperties(rtPhonetics);
+ }
+ else
+ {
+ _phoneticRuns = Array.Empty();
+ PhoneticsProperties = null;
+ }
+ }
+
+ ///
+ /// A text of a whole rich text, without styling.
+ ///
+ public string Text { get; }
+
+ ///
+ /// Individual rich text runs that make up the , in ascending order, non-overlapping.
+ ///
+ public IReadOnlyList Runs => _runs;
+
+ ///
+ /// All phonetics runs of rich text. Empty array, if no phonetic run. In ascending order, non-overlapping.
+ ///
+ public IReadOnlyList PhoneticRuns => _phoneticRuns;
+
+ ///
+ /// Properties used to display phonetic runs.
+ ///
+ public PhoneticProperties? PhoneticsProperties { get; }
+
+ public bool Equals(XLImmutableRichText? other)
+ {
+ if (other is null)
+ return false;
+
+ if (ReferenceEquals(this, other))
+ return true;
+
+ return Text == other.Text &&
+ _runs.SequenceEqual(other._runs) &&
+ _phoneticRuns.SequenceEqual(other._phoneticRuns) &&
+ Nullable.Equals(PhoneticsProperties, other.PhoneticsProperties);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as XLImmutableRichText);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Text.GetHashCode();
+ hashCode = (hashCode * 397) ^ PhoneticsProperties.GetHashCode();
+ foreach (var phoneticRun in _phoneticRuns)
+ hashCode = (hashCode * 397) ^ phoneticRun.GetHashCode();
+
+ foreach (var run in _runs)
+ hashCode = (hashCode * 397) ^ run.GetHashCode();
+
+ return hashCode;
+ }
+ }
+
+ internal string GetRunText(RichTextRun run) => Text.Substring(run.StartIndex, run.Length);
+
+ internal readonly struct RichTextRun : IEquatable
+ {
+ internal readonly int StartIndex;
+ internal readonly int Length;
+ internal readonly XLFontValue Font;
+
+ internal RichTextRun(XLRichString richString, int startIndex, int length)
+ {
+ var key = XLFont.GenerateKey(richString);
+ Font = XLFontValue.FromKey(ref key);
+ StartIndex = startIndex;
+ Length = length;
+ }
+
+ public bool Equals(RichTextRun other)
+ {
+ return StartIndex == other.StartIndex && Length == other.Length && Font.Equals(other.Font);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is RichTextRun other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = StartIndex;
+ hashCode = (hashCode * 397) ^ Length;
+ hashCode = (hashCode * 397) ^ Font.GetHashCode();
+ return hashCode;
+ }
+ }
+ }
+
+ ///
+ /// Phonetic runs can't overlap and must be in order (i.e. start index must be ascending).
+ ///
+ internal readonly struct PhoneticRun
+ {
+ ///
+ /// Text that is displayed above a segment indicating how should segment be read.
+ ///
+ internal readonly string Text;
+
+ ///
+ /// Starting index of displayed phonetic (first character is 0).
+ ///
+ internal readonly int StartIndex;
+
+ ///
+ /// End index, excluding (the last index is a length of the rich text).
+ ///
+ internal readonly int EndIndex;
+
+ public PhoneticRun(string text, int startIndex, int endIndex)
+ {
+ if (text.Length == 0)
+ throw new ArgumentException("Phonetic run text can't be empty.", nameof(text));
+
+ if (startIndex < 0)
+ throw new ArgumentException("Start index index must be greater than 0.", nameof(startIndex));
+
+ if (startIndex >= endIndex)
+ throw new ArgumentException("Start index must be less than end index.", nameof(endIndex));
+
+ Text = text;
+ StartIndex = startIndex;
+ EndIndex = endIndex;
+ }
+
+ public bool Equals(PhoneticRun other)
+ {
+ return Text == other.Text && StartIndex == other.StartIndex && EndIndex == other.EndIndex;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is PhoneticRun other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Text.GetHashCode();
+ hashCode = (hashCode * 397) ^ StartIndex;
+ hashCode = (hashCode * 397) ^ EndIndex;
+ return hashCode;
+ }
+ }
+ }
+
+ ///
+ /// Properties of phonetic runs. All phonetic runs of a rich text have same font and other properties.
+ ///
+ internal readonly struct PhoneticProperties
+ {
+ ///
+ /// Font used for text of phonetic runs. All phonetic runs use same font. There can be no phonetic runs,
+ /// but with specified font (e.g. the mutable API has only specified font, but no text yet).
+ ///
+ public readonly XLFontValue Font;
+
+ ///
+ /// Type of phonetics. Default is
+ ///
+ public readonly XLPhoneticType Type;
+
+ ///
+ /// Alignment of phonetics. Default is
+ ///
+ public readonly XLPhoneticAlignment Alignment;
+
+ public PhoneticProperties(XLPhonetics rtPhonetics)
+ {
+ var fontKey = XLFont.GenerateKey(rtPhonetics);
+ Font = XLFontValue.FromKey(ref fontKey);
+ Type = rtPhonetics.Type;
+ Alignment = rtPhonetics.Alignment;
+ }
+
+ public bool Equals(PhoneticProperties other)
+ {
+ return Font.Equals(other.Font) && Type == other.Type && Alignment == other.Alignment;
+ }
+
+ public override bool Equals(object? obj)
+ {
+ return obj is PhoneticProperties other && Equals(other);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = Font.GetHashCode();
+ hashCode = (hashCode * 397) ^ (int)Type;
+ hashCode = (hashCode * 397) ^ (int)Alignment;
+ return hashCode;
+ }
+ }
+ }
+ }
+}
diff --git a/ClosedXML/Excel/RichText/XLPhonetic.cs b/ClosedXML/Excel/RichText/XLPhonetic.cs
index ee8535479..37c2f1723 100644
--- a/ClosedXML/Excel/RichText/XLPhonetic.cs
+++ b/ClosedXML/Excel/RichText/XLPhonetic.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
namespace ClosedXML.Excel
@@ -12,15 +10,18 @@ public XLPhonetic(String text, Int32 start, Int32 end)
Start = start;
End = end;
}
- public String Text { get; set; }
- public Int32 Start { get; set; }
- public Int32 End { get; set; }
+ public String Text { get; }
+ public Int32 Start { get; }
+ public Int32 End { get; }
- public bool Equals(IXLPhonetic other)
+ public bool Equals(IXLPhonetic? other)
{
- if (other == null)
+ if (other is null)
return false;
+ if (ReferenceEquals(this, other))
+ return true;
+
return Text == other.Text && Start == other.Start && End == other.End;
}
}
diff --git a/ClosedXML/Excel/RichText/XLPhonetics.cs b/ClosedXML/Excel/RichText/XLPhonetics.cs
index 4617cc758..821998ce8 100644
--- a/ClosedXML/Excel/RichText/XLPhonetics.cs
+++ b/ClosedXML/Excel/RichText/XLPhonetics.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.Linq;
@@ -8,39 +6,172 @@ namespace ClosedXML.Excel
{
internal class XLPhonetics : IXLPhonetics
{
- private readonly List _phonetics = new List();
-
+ private readonly List _phonetics = new();
+ private readonly XLFont _font;
private readonly IXLFontBase _defaultFont;
+ private readonly Action _onChange;
+ private XLPhoneticAlignment _alignment;
+ private XLPhoneticType _type;
- public XLPhonetics(IXLFontBase defaultFont)
+ public XLPhonetics(IXLFontBase defaultFont, Action onChange)
{
_defaultFont = defaultFont;
- Type = XLPhoneticType.FullWidthKatakana;
- Alignment = XLPhoneticAlignment.Left;
- this.CopyFont(_defaultFont);
+ _font = new XLFont(defaultFont);
+ _type = XLPhoneticType.FullWidthKatakana;
+ _alignment = XLPhoneticAlignment.Left;
+ _onChange = onChange;
}
- public XLPhonetics(IXLPhonetics defaultPhonetics, IXLFontBase defaultFont)
+ public XLPhonetics(IXLPhonetics defaultPhonetics, IXLFontBase defaultFont, Action onChange)
{
_defaultFont = defaultFont;
- Type = defaultPhonetics.Type;
- Alignment = defaultPhonetics.Alignment;
+ _font = new XLFont(defaultPhonetics);
+ _type = defaultPhonetics.Type;
+ _alignment = defaultPhonetics.Alignment;
+ _onChange = onChange;
+ }
+
+ public Int32 Count => _phonetics.Count;
+
+ public Boolean Bold
+ {
+ get => _font.Bold;
+ set
+ {
+ _font.Bold = value;
+ _onChange();
+ }
+ }
+
+ public Boolean Italic
+ {
+ get => _font.Italic;
+ set
+ {
+ _font.Italic = value;
+ _onChange();
+ }
+ }
+
+ public XLFontUnderlineValues Underline
+ {
+ get => _font.Underline;
+ set
+ {
+ _font.Underline = value;
+ _onChange();
+ }
+ }
+
+ public Boolean Strikethrough
+ {
+ get => _font.Strikethrough;
+ set
+ {
+ _font.Strikethrough = value;
+ _onChange();
+ }
+ }
+
+ public XLFontVerticalTextAlignmentValues VerticalAlignment
+ {
+ get => _font.VerticalAlignment;
+ set
+ {
+ _font.VerticalAlignment = value;
+ _onChange();
+ }
+ }
+
+ public Boolean Shadow
+ {
+ get => _font.Shadow;
+ set
+ {
+ _font.Shadow = value;
+ _onChange();
+ }
+ }
- this.CopyFont(defaultPhonetics);
+ public Double FontSize
+ {
+ get => _font.FontSize;
+ set
+ {
+ _font.FontSize = value;
+ _onChange();
+ }
}
- public Boolean Bold { get; set; }
- public Boolean Italic { get; set; }
- public XLFontUnderlineValues Underline { get; set; }
- public Boolean Strikethrough { get; set; }
- public XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; }
- public Boolean Shadow { get; set; }
- public Double FontSize { get; set; }
- public XLColor FontColor { get; set; }
- public String FontName { get; set; }
- public XLFontFamilyNumberingValues FontFamilyNumbering { get; set; }
- public XLFontCharSet FontCharSet { get; set; }
- public XLFontScheme FontScheme { get; set; }
+ public XLColor FontColor
+ {
+ get => _font.FontColor;
+ set
+ {
+ _font.FontColor = value;
+ _onChange();
+ }
+ }
+
+ public String FontName
+ {
+ get => _font.FontName;
+ set
+ {
+ _font.FontName = value;
+ _onChange();
+ }
+ }
+
+ public XLFontFamilyNumberingValues FontFamilyNumbering
+ {
+ get => _font.FontFamilyNumbering;
+ set
+ {
+ _font.FontFamilyNumbering = value;
+ _onChange();
+ }
+ }
+
+ public XLFontCharSet FontCharSet
+ {
+ get => _font.FontCharSet;
+ set
+ {
+ _font.FontCharSet = value;
+ _onChange();
+ }
+ }
+
+ public XLFontScheme FontScheme
+ {
+ get => _font.FontScheme;
+ set
+ {
+ _font.FontScheme = value;
+ _onChange();
+ }
+ }
+
+ public XLPhoneticAlignment Alignment
+ {
+ get => _alignment;
+ set
+ {
+ _alignment = value;
+ _onChange();
+ }
+ }
+
+ public XLPhoneticType Type
+ {
+ get => _type;
+ set
+ {
+ _type = value;
+ _onChange();
+ }
+ }
public IXLPhonetics SetBold() { Bold = true; return this; }
@@ -76,33 +207,31 @@ public XLPhonetics(IXLPhonetics defaultPhonetics, IXLFontBase defaultFont)
public IXLPhonetics SetFontScheme(XLFontScheme value) { FontScheme = value; return this; }
+ public IXLPhonetics SetAlignment(XLPhoneticAlignment phoneticAlignment) { Alignment = phoneticAlignment; return this; }
+
+ public IXLPhonetics SetType(XLPhoneticType phoneticType) { Type = phoneticType; return this; }
+
public IXLPhonetics Add(String text, Int32 start, Int32 end)
{
_phonetics.Add(new XLPhonetic(text, start, end));
+ _onChange();
return this;
}
public IXLPhonetics ClearText()
{
_phonetics.Clear();
+ _onChange();
return this;
}
public IXLPhonetics ClearFont()
{
this.CopyFont(_defaultFont);
+ _onChange();
return this;
}
- public Int32 Count { get { return _phonetics.Count; } }
-
- public XLPhoneticAlignment Alignment { get; set; }
- public XLPhoneticType Type { get; set; }
-
- public IXLPhonetics SetAlignment(XLPhoneticAlignment phoneticAlignment) { Alignment = phoneticAlignment; return this; }
-
- public IXLPhonetics SetType(XLPhoneticType phoneticType) { Type = phoneticType; return this; }
-
public IEnumerator GetEnumerator()
{
return _phonetics.GetEnumerator();
@@ -113,30 +242,23 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
return GetEnumerator();
}
- public bool Equals(IXLPhonetics other)
+ public bool Equals(IXLPhonetics? other) => Equals(other as XLPhonetics);
+
+ public bool Equals(XLPhonetics? other)
{
- if (other == null)
+ if (other is null)
return false;
- Int32 phoneticsCount = _phonetics.Count;
- for (Int32 i = 0; i < phoneticsCount; i++)
- {
- if (!_phonetics[i].Equals(other.ElementAt(i)))
- return false;
- }
+ if (ReferenceEquals(this, other))
+ return true;
+
+ if (!_phonetics.SequenceEqual(other._phonetics))
+ return false;
return
- Bold == other.Bold
- && Italic == other.Italic
- && Underline == other.Underline
- && Strikethrough == other.Strikethrough
- && VerticalAlignment == other.VerticalAlignment
- && Shadow == other.Shadow
- && FontSize.Equals(other.FontSize)
- && FontColor.Equals(other.FontColor)
- && FontName == other.FontName
- && FontFamilyNumbering == other.FontFamilyNumbering
- && FontScheme == other.FontScheme;
+ _font.Key.Equals(other._font.Key) &&
+ Type == other.Type &&
+ Alignment == other.Alignment;
}
}
}
diff --git a/ClosedXML/Excel/RichText/XLRichString.cs b/ClosedXML/Excel/RichText/XLRichString.cs
index 8bbbe9bcc..a564a7bb0 100644
--- a/ClosedXML/Excel/RichText/XLRichString.cs
+++ b/ClosedXML/Excel/RichText/XLRichString.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Diagnostics;
@@ -9,12 +7,15 @@ namespace ClosedXML.Excel
internal class XLRichString : IXLRichString
{
private readonly IXLWithRichString _withRichString;
+ private readonly XLFont _font;
+ private readonly Action _onChange;
- public XLRichString(String text, IXLFontBase font, IXLWithRichString withRichString)
+ public XLRichString(String text, IXLFontBase font, IXLWithRichString withRichString, Action? onChange)
{
Text = text;
- this.CopyFont(font);
+ _font = new XLFont(font);
_withRichString = withRichString;
+ _onChange = onChange ?? (() => { });
}
public String Text { get; set; }
@@ -29,18 +30,125 @@ public IXLRichString AddNewLine()
return AddText(Environment.NewLine);
}
- public Boolean Bold { get; set; }
- public Boolean Italic { get; set; }
- public XLFontUnderlineValues Underline { get; set; }
- public Boolean Strikethrough { get; set; }
- public XLFontVerticalTextAlignmentValues VerticalAlignment { get; set; }
- public Boolean Shadow { get; set; }
- public Double FontSize { get; set; }
- public XLColor FontColor { get; set; }
- public String FontName { get; set; }
- public XLFontFamilyNumberingValues FontFamilyNumbering { get; set; }
- public XLFontCharSet FontCharSet { get; set; }
- public XLFontScheme FontScheme { get; set; }
+ public Boolean Bold
+ {
+ get => _font.Bold;
+ set
+ {
+ _font.Bold = value;
+ _onChange();
+ }
+ }
+
+ public Boolean Italic
+ {
+ get => _font.Italic;
+ set
+ {
+ _font.Italic = value;
+ _onChange();
+ }
+ }
+
+ public XLFontUnderlineValues Underline
+ {
+ get => _font.Underline;
+ set
+ {
+ _font.Underline = value;
+ _onChange();
+ }
+ }
+
+ public Boolean Strikethrough
+ {
+ get => _font.Strikethrough;
+ set
+ {
+ _font.Strikethrough = value;
+ _onChange();
+ }
+ }
+
+ public XLFontVerticalTextAlignmentValues VerticalAlignment
+ {
+ get => _font.VerticalAlignment;
+ set
+ {
+ _font.VerticalAlignment = value;
+ _onChange();
+ }
+ }
+
+ public Boolean Shadow
+ {
+ get => _font.Shadow;
+ set
+ {
+ _font.Shadow = value;
+ _onChange();
+ }
+ }
+
+ public Double FontSize
+ {
+ get => _font.FontSize;
+ set
+ {
+ _font.FontSize = value;
+ _onChange();
+ }
+ }
+
+ public XLColor FontColor
+ {
+ get => _font.FontColor;
+ set
+ {
+ _font.FontColor = value;
+ _onChange();
+ }
+ }
+
+ public String FontName
+ {
+ get => _font.FontName;
+ set
+ {
+ _font.FontName = value;
+ _onChange();
+ }
+ }
+
+ public XLFontFamilyNumberingValues FontFamilyNumbering
+ {
+ get => _font.FontFamilyNumbering;
+ set
+ {
+ _font.FontFamilyNumbering = value;
+ _onChange();
+ }
+ }
+
+ public XLFontCharSet FontCharSet
+ {
+ get => _font.FontCharSet;
+ set
+ {
+ _font.FontCharSet = value;
+ _onChange();
+ }
+ }
+
+ public XLFontScheme FontScheme
+ {
+ get => _font.FontScheme;
+ set
+ {
+ _font.FontScheme = value;
+ _onChange();
+ }
+ }
public IXLRichString SetBold()
{
@@ -127,42 +235,26 @@ public IXLRichString SetFontScheme(XLFontScheme value)
FontScheme = value; return this;
}
- public Boolean Equals(IXLRichString other)
- {
- return
- Text == other.Text
- && Bold.Equals(other.Bold)
- && Italic.Equals(other.Italic)
- && Underline.Equals(other.Underline)
- && Strikethrough.Equals(other.Strikethrough)
- && VerticalAlignment.Equals(other.VerticalAlignment)
- && Shadow.Equals(other.Shadow)
- && FontSize.Equals(other.FontSize)
- && FontColor.Equals(other.FontColor)
- && FontName.Equals(other.FontName)
- && FontFamilyNumbering.Equals(other.FontFamilyNumbering)
- && FontScheme.Equals(other.FontScheme)
- ;
- }
+ public override bool Equals(object obj) => Equals(obj as XLRichString);
+
+ public Boolean Equals(IXLRichString? other) => Equals(other as XLRichString);
- public override bool Equals(object obj)
+ public Boolean Equals(XLRichString? other)
{
- return Equals((XLRichString)obj);
+ if (other is null)
+ return false;
+
+ if (ReferenceEquals(this, other))
+ return true;
+
+ return Text == other.Text && _font.Key.Equals(other._font.Key);
}
public override int GetHashCode()
{
- return Text.GetHashCode()
- ^ Bold.GetHashCode()
- ^ Italic.GetHashCode()
- ^ (Int32)Underline
- ^ Strikethrough.GetHashCode()
- ^ (Int32)VerticalAlignment
- ^ Shadow.GetHashCode()
- ^ FontSize.GetHashCode()
- ^ FontColor.GetHashCode()
- ^ FontName.GetHashCode()
- ^ (Int32)FontFamilyNumbering;
+ // Since all properties of type are mutable, can't have different hashcode for any instance.
+ // Don't ever use this class in a dictionary, e.g. SST.
+ return 4; // Chosen by fair dice roll. Guaranteed to be random.
}
}
}
diff --git a/ClosedXML/Excel/RichText/XLRichText.cs b/ClosedXML/Excel/RichText/XLRichText.cs
index d2165858d..097729f0b 100644
--- a/ClosedXML/Excel/RichText/XLRichText.cs
+++ b/ClosedXML/Excel/RichText/XLRichText.cs
@@ -1,23 +1,49 @@
-#nullable disable
-
using System;
+using System.Linq;
namespace ClosedXML.Excel
{
internal class XLRichText : XLFormattedText, IXLRichText
{
- private readonly XLCell _cell;
+ // Should be set as the last thing in ctor to prevent firing changes to immutable rich text during ctor
+ private readonly XLCell? _cell;
- public XLRichText(XLCell cell, IXLFontBase defaultFont)
- : base(defaultFont)
+ ///
+ /// Copy ctor to return user modifiable rich text from immutable rich text stored
+ /// in the shared string table.
+ ///
+ public XLRichText(XLCell cell, XLImmutableRichText original)
+ : base(cell.Style.Font)
{
+ foreach (var originalRun in original.Runs)
+ {
+ var runText = original.GetRunText(originalRun);
+ AddText(new XLRichString(runText, new XLFont(originalRun.Font.Key), this, OnContentChanged));
+ }
+
+ var hasPhonetics = original.PhoneticRuns.Any() || original.PhoneticsProperties.HasValue;
+ if (hasPhonetics)
+ {
+ // Access to phonetics instantiate a new instance.
+ var phonetics = Phonetics;
+ if (original.PhoneticsProperties.HasValue)
+ {
+ var phoneticProps = original.PhoneticsProperties.Value;
+ phonetics.CopyFont(new XLFont(phoneticProps.Font.Key));
+ phonetics.Type = phoneticProps.Type;
+ phonetics.Alignment = phoneticProps.Alignment;
+ }
+
+ foreach (var phoneticRun in original.PhoneticRuns)
+ phonetics.Add(phoneticRun.Text, phoneticRun.StartIndex, phoneticRun.EndIndex);
+ }
+
Container = this;
_cell = cell;
- ContentChanged += OnContentChanged;
}
- public XLRichText(XLCell cell, XLFormattedText defaultRichText, IXLFontBase defaultFont)
- : base(defaultRichText, defaultFont)
+ public XLRichText(XLCell cell, IXLFontBase defaultFont)
+ : base(defaultFont)
{
Container = this;
_cell = cell;
@@ -30,12 +56,19 @@ public XLRichText(XLCell cell, String text, IXLFontBase defaultFont)
_cell = cell;
}
- private void OnContentChanged(object sender, EventArgs e)
+ protected override void OnContentChanged()
{
- if (_cell.DataType != XLDataType.Text || !_cell.HasRichText || !ReferenceEquals(_cell.GetRichText(), this))
+ // The rich text is still being created
+ if (_cell is null)
+ return;
+
+ if (_cell.DataType != XLDataType.Text || !_cell.HasRichText)
throw new InvalidOperationException("The rich text isn't a content of a cell.");
_cell.SetOnlyValue(Text);
+ var point = _cell.SheetPoint;
+ var richText = new XLImmutableRichText(this);
+ _cell.Worksheet.Internals.CellsCollection.ValueSlice.SetRichText(point, richText);
}
}
}
diff --git a/ClosedXML/Excel/Style/XLFont.cs b/ClosedXML/Excel/Style/XLFont.cs
index cc34f6247..e80b6f3ab 100644
--- a/ClosedXML/Excel/Style/XLFont.cs
+++ b/ClosedXML/Excel/Style/XLFont.cs
@@ -1,4 +1,5 @@
using System;
+using System.Globalization;
using System.Text;
namespace ClosedXML.Excel
@@ -82,7 +83,26 @@ public XLFont(XLStyle? style, XLFontKey key) : this(style, XLFontValue.FromKey(r
{
}
- public XLFont(XLStyle? style = null, IXLFont? d = null) : this(style, GenerateKey(d))
+ ///
+ /// Create a new font that is attached to a style and the changes to the font object are propagated to the style.
+ ///
+ /// The container style that will be modified by changes of created XLFont.
+ public XLFont(XLStyle style) : this(style, GenerateKey(style.Font))
+ {
+ }
+
+ ///
+ /// Create a new font. The changes to the object are not propagated to a style.
+ ///
+ public XLFont(IXLFontBase font) : this(null, GenerateKey(font))
+ {
+ }
+
+ public XLFont(XLFontKey key) : this(null, XLFontValue.FromKey(ref key))
+ {
+ }
+
+ private XLFont() : this(null, GenerateKey(null))
{
}
@@ -337,7 +357,7 @@ public override string ToString()
sb.Append("-");
sb.Append(Shadow.ToString());
sb.Append("-");
- sb.Append(FontSize.ToString());
+ sb.Append(FontSize.ToString(CultureInfo.InvariantCulture));
sb.Append("-");
sb.Append(FontColor);
sb.Append("-");
diff --git a/ClosedXML/Extensions/FontBaseExtensions.cs b/ClosedXML/Extensions/FontBaseExtensions.cs
index 35432094c..8c71b104f 100644
--- a/ClosedXML/Extensions/FontBaseExtensions.cs
+++ b/ClosedXML/Extensions/FontBaseExtensions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
// Keep this file CodeMaid organised and cleaned
namespace ClosedXML.Excel
@@ -19,6 +17,7 @@ public static void CopyFont(this IXLFontBase font, IXLFontBase sourceFont)
font.FontName = sourceFont.FontName;
font.FontFamilyNumbering = sourceFont.FontFamilyNumbering;
font.FontCharSet = sourceFont.FontCharSet;
+ font.FontScheme = sourceFont.FontScheme;
}
}
}
diff --git a/docs/migrations/migrate-to-0.103.rst b/docs/migrations/migrate-to-0.103.rst
new file mode 100644
index 000000000..37997afd2
--- /dev/null
+++ b/docs/migrations/migrate-to-0.103.rst
@@ -0,0 +1,14 @@
+#############################
+Migration from 0.102 to 0.103
+#############################
+
+***********
+IXLPhonetic
+***********
+
+``IXLPhonetic`` no longer has a setter for its ``IXLPhonetic.Text``,
+``IXLPhonetic.Start`` and ``IXLPhonetic.End`` properties.
+
+Use ``IXLPhonetics.ClearText()`` and ``IXLPhonetics.Add(String text, Int32 start, Int32 end)``
+to redo the phonetic hints.
+