NanoXLSX.Formatting 3.0.0
Loading...
Searching...
No Matches
FormattedText.cs
1/*
2 * NanoXLSX is a small .NET library to generate and read XLSX (Microsoft Excel 2007 or newer) files in an easy and native way
3 * Copyright Raphael Stoeckli © 2026
4 * This library is licensed under the MIT License.
5 * You find a copy of the license in project folder or on: http://opensource.org/licenses/MIT
6 */
7
8using System;
9using System.Collections.Generic;
10using System.Linq;
11using NanoXLSX.Colors;
13using NanoXLSX.Interfaces;
14using NanoXLSX.Styles;
15using NanoXLSX.Utils;
16using NanoXLSX.Utils.Xml;
17using static NanoXLSX.Styles.Font;
18using FormatException = NanoXLSX.Exceptions.FormatException;
19
20namespace NanoXLSX
21{
25 public class FormattedText : IFormattableText
26 {
27 #region constants
28 private const string SI_TAG = "si";
29 private const string R_TAG = "r";
30 private const string T_TAG = "t";
31 private const string RPR_TAG = "rPr";
32 private const string RPH_TAG = "rPh";
33 private const string PHONETIC_PR_TAG = "phoneticPr";
34 private const string PRESERVE_ATTRIBUTE_NAME = "space";
35 private const string PRESERVE_ATTRIBUTE_PREFIX = "xml";
36 private const string PRESERVE_ATTRIBUTE_VALUE = "preserve";
37 #endregion
38
39 #region staticFields
40
44 public static readonly Style LineBreakStyle;
45
46 static FormattedText()
47 {
48 LineBreakStyle = new Style();
49 LineBreakStyle.CurrentCellXf.Alignment = CellXf.TextBreakValue.WrapText;
50 }
51 #endregion
52
53 #region privateFields
54 private readonly List<TextRun> runs = new List<TextRun>();
55 private readonly List<PhoneticRun> phoeticRuns = new List<PhoneticRun>();
56 private string overridePlainText = null;
57 #endregion
58
59 #region properties
64 public bool WrapText { get; set; }
65
69 public IReadOnlyList<TextRun> Runs => runs.AsReadOnly();
73 public IReadOnlyList<PhoneticRun> PhoneticRuns => phoeticRuns.AsReadOnly();
78
82 public string PlainText => overridePlainText != null ? overridePlainText : string.Concat(runs.Select(r => r.Text));
83 #endregion
84
85 #region methods
86
93 public FormattedText AddRun(string text, Font fontStyle = null)
94 {
95 if (string.IsNullOrEmpty(text))
96 {
97 throw new FormatException("Text cannot be null or empty");
98 }
99 CheckWrapText(text);
100 runs.Add(new TextRun(text, fontStyle));
101 return this;
102 }
103
110 public FormattedText AddRun(string text, Action<InlineStyleBuilder> styleBuilder)
111 {
112 if (string.IsNullOrEmpty(text))
113 {
114 throw new FormatException("Text cannot be null or empty");
115 }
116 CheckWrapText(text);
117 var builder = new InlineStyleBuilder();
118 styleBuilder?.Invoke(builder);
119 runs.Add(new TextRun(text, builder.Build()));
120 return this;
121 }
122
127 public void AddLineBreak(bool useStyleFromLastRun = true)
128 {
129 if (runs.Count == 0 || !useStyleFromLastRun)
130 {
131 runs.Add(new TextRun(Environment.NewLine, null));
132 }
133 else
134 {
135 runs[runs.Count - 1].Text += Environment.NewLine;
136 }
137 WrapText = true;
138 }
139
147 public FormattedText AddPhoneticRun(string text, uint startBase, uint endBase)
148 {
149 phoeticRuns.Add(new PhoneticRun(text, startBase, endBase));
150 return this;
151 }
152
160 public FormattedText SetPhoneticProperties(Font fontReference, PhoneticRun.PhoneticType type = PhoneticRun.PhoneticType.FullwidthKatakana, PhoneticRun.PhoneticAlignment alignment = PhoneticRun.PhoneticAlignment.Left)
161 {
162 PhoneticProperties = new PhoneticProperties(fontReference)
163 {
164 Type = type,
165 Alignment = alignment
166 };
167 return this;
168 }
169
173 public void Clear()
174 {
175 runs.Clear();
176 phoeticRuns.Clear();
177 PhoneticProperties = null;
178 overridePlainText = null;
179 }
180
185 public FormattedText Copy()
186 {
187 FormattedText copy = new FormattedText();
188 foreach (TextRun run in runs)
189 {
190 copy.runs.Add(run.Copy());
191 }
192 foreach (PhoneticRun phoneticRun in phoeticRuns)
193 {
194 copy.phoeticRuns.Add(phoneticRun.Copy());
195 }
196 if (PhoneticProperties != null)
197 {
198 copy.PhoneticProperties = PhoneticProperties.Copy();
199 }
200 copy.overridePlainText = overridePlainText;
201 return copy;
202 }
203
208 override public string ToString()
209 {
210 return PlainText;
211 }
212
217 internal void OverridePlainText(string value)
218 {
219 this.overridePlainText = value;
220 }
221
222 #endregion
223
224 #region privateMethods
225
230 internal XmlElement GetXmlElement()
231 {
232 XmlElement siElement = XmlElement.CreateElement(SI_TAG);
233
234 if (Runs.Count == 0)
235 {
236 return siElement;
237 }
238
239 foreach (TextRun run in Runs)
240 {
241 XmlElement rElement = siElement.AddChildElement(R_TAG);
242
243 if (run.FontStyle != null)
244 {
245 XmlElement rPrElement = CreateRunPropertiesElement(run.FontStyle);
246 rElement.AddChildElement(rPrElement);
247 }
248
249 XmlElement tElement = CreateTextElement(run.Text);
250 rElement.AddChildElement(tElement);
251 }
252
253 foreach (var phoneticRun in PhoneticRuns)
254 {
255 XmlElement rPhElement = CreatePhoneticRunElement(phoneticRun);
256 siElement.AddChildElement(rPhElement);
257 }
258
259 if (PhoneticProperties != null)
260 {
261 XmlElement phoneticPrElement = CreatePhoneticPropertiesElement(PhoneticProperties);
262 siElement.AddChildElement(phoneticPrElement);
263 }
264
265 return siElement;
266 }
267
272 private void CheckWrapText(string text)
273 {
274 if (text.Contains("\n"))
275 {
276 WrapText = true;
277 }
278 }
279
284 XmlElement IFormattableText.GetXmlElement()
285 {
286 return GetXmlElement();
287 }
288
289
290
295 private static XmlElement CreatePhoneticPropertiesElement(PhoneticProperties properties)
296 {
297 XmlElement phoneticPrElement = XmlElement.CreateElement(PHONETIC_PR_TAG);
298 phoneticPrElement.AddAttribute("fontId", ParserUtils.ToString(properties.FontId));
299 string typeValue = GetPhoneticTypeValue(properties.Type);
300 phoneticPrElement.AddAttribute("type", typeValue);
301 string alignmentValue = GetPhoneticAlignmentValue(properties.Alignment);
302 phoneticPrElement.AddAttribute("alignment", alignmentValue);
303 return phoneticPrElement;
304 }
305
306
312 private static XmlElement CreateRunPropertiesElement(Font fontStyle)
313 {
314 XmlElement rPrElement = XmlElement.CreateElement(RPR_TAG);
315 if (!string.IsNullOrEmpty(fontStyle.Name))
316 {
317 rPrElement.AddChildElementWithAttribute("rFont", "val", fontStyle.Name);
318 }
319 if (fontStyle.Charset != Font.CharsetValue.Default)
320 {
321 rPrElement.AddChildElementWithAttribute("charset", "val", ParserUtils.ToString((int)fontStyle.Charset));
322 }
323 if (fontStyle.Family != Font.DefaultFontFamily)
324 {
325 rPrElement.AddChildElementWithAttribute("family", "val", ParserUtils.ToString((int)fontStyle.Family));
326 }
327 if (fontStyle.Bold)
328 {
329 rPrElement.AddChildElement("b");
330 }
331 if (fontStyle.Italic)
332 {
333 rPrElement.AddChildElement("i");
334 }
335 if (fontStyle.Strike)
336 {
337 rPrElement.AddChildElement("strike");
338 }
339 if (fontStyle.Outline)
340 {
341 rPrElement.AddChildElement("outline");
342 }
343 if (fontStyle.Shadow)
344 {
345 rPrElement.AddChildElement("shadow");
346 }
347 if (fontStyle.Condense)
348 {
349 rPrElement.AddChildElement("condense");
350 }
351 if (fontStyle.Extend)
352 {
353 rPrElement.AddChildElement("extend");
354 }
355 if (fontStyle.ColorValue != null && fontStyle.ColorValue.IsDefined)
356 {
357 XmlElement colorElement = CreateColorElement(fontStyle.ColorValue);
358 rPrElement.AddChildElement(colorElement);
359 }
360 if (fontStyle.Size != Font.DefaultFontSize)
361 {
362 rPrElement.AddChildElementWithAttribute("sz", "val", ParserUtils.ToString(fontStyle.Size));
363 }
364 if (fontStyle.Underline != UnderlineValue.None && fontStyle.Underline != UnderlineValue.Single)
365 {
366 rPrElement.AddChildElementWithAttribute("u", "val", Font.GetUnderlineName(fontStyle.Underline));
367 }
368 else if (fontStyle.Underline == UnderlineValue.Single)
369 {
370 rPrElement.AddChildElement("u");
371 }
372 if (fontStyle.VerticalAlign != Font.VerticalTextAlignValue.None)
373 {
374 string vertAlignValue = Font.GetVerticalTextAlignName(fontStyle.VerticalAlign);
375 rPrElement.AddChildElementWithAttribute("vertAlign", "val", vertAlignValue);
376 }
377 if (fontStyle.Scheme != Font.SchemeValue.None)
378 {
379 if (fontStyle.Scheme == SchemeValue.Major)
380 {
381 rPrElement.AddChildElementWithAttribute("scheme", "val", "major");
382 }
383 else if (fontStyle.Scheme == SchemeValue.Minor)
384 { rPrElement.AddChildElementWithAttribute("scheme", "val", "minor"); }
385 }
386 return rPrElement;
387 }
388
395 private static XmlElement CreateTextElement(string text)
396 {
397 string value = XmlUtils.SanitizeXmlValue(text);
398 value = ParserUtils.NormalizeNewLines(value);
399 XmlElement element;
400 if (char.IsWhiteSpace(value, 0) || char.IsWhiteSpace(value, value.Length - 1))
401 {
402 element = XmlElement.CreateElementWithAttribute(
403 T_TAG,
404 PRESERVE_ATTRIBUTE_NAME,
405 PRESERVE_ATTRIBUTE_VALUE,
406 "",
407 PRESERVE_ATTRIBUTE_PREFIX);
408 }
409 else
410 {
411 element = XmlElement.CreateElement(T_TAG);
412 }
413 element.InnerValue = value;
414 return element;
415 }
416
422 private static XmlElement CreateColorElement(Color color)
423 {
424 XmlElement colorElement = XmlElement.CreateElement("color");
425 if (color.Value is AutoColor)
426 {
427 colorElement.AddAttribute("auto", "1");
428 }
429 else if (color.Value is IndexedColor)
430 {
431 colorElement.AddAttribute("indexed", color.Value.StringValue);
432 }
433 else if (color.Value is SrgbColor)
434 {
435 colorElement.AddAttribute("rgb", color.Value.StringValue);
436 }
437 else if (color.Value is ThemeColor)
438 {
439 colorElement.AddAttribute("theme", color.Value.StringValue);
440 }
441 else if (color.Value is SystemColor)
442 {
443 colorElement.AddAttribute("system", color.Value.StringValue);
444 }
445 if (color.Tint.HasValue)
446 {
447 colorElement.AddAttribute("tint", ParserUtils.ToString(color.Tint.Value));
448 }
449 return colorElement;
450 }
451
457 private static XmlElement CreatePhoneticRunElement(PhoneticRun phoneticRun)
458 {
459 XmlElement rPhElement = XmlElement.CreateElement(RPH_TAG);
460 rPhElement.AddAttribute("sb", ParserUtils.ToString(phoneticRun.StartBase));
461 rPhElement.AddAttribute("eb", ParserUtils.ToString(phoneticRun.EndBase));
462 XmlElement tElement = CreateTextElement(phoneticRun.Text);
463 rPhElement.AddChildElement(tElement);
464 return rPhElement;
465 }
466
472 private static string GetPhoneticTypeValue(PhoneticRun.PhoneticType type)
473 {
474 switch (type)
475 {
476 case PhoneticRun.PhoneticType.HalfwidthKatakana:
477 return "halfwidthKatakana";
478 case PhoneticRun.PhoneticType.Hiragana:
479 return "Hiragana";
480 case PhoneticRun.PhoneticType.NoConversion:
481 return "noConversion";
482 default:
483 return "fullwidthKatakana";
484 }
485 }
486
492 private static string GetPhoneticAlignmentValue(PhoneticRun.PhoneticAlignment alignment)
493 {
494 switch (alignment)
495 {
496 case PhoneticRun.PhoneticAlignment.NoControl:
497 return "noControl";
498 case PhoneticRun.PhoneticAlignment.Center:
499 return "center";
500 case PhoneticRun.PhoneticAlignment.Distributed:
501 return "distributed";
502 default:
503 return "left";
504 }
505 }
506
512 public override bool Equals(object obj)
513 {
514 if (!(obj is FormattedText text))
515 return false;
516
517 return runs.SequenceEqual(text.runs) &&
518 phoeticRuns.SequenceEqual(text.phoeticRuns) &&
519 WrapText == text.WrapText &&
520 overridePlainText == text.overridePlainText &&
521 EqualityComparer<PhoneticProperties>.Default.Equals(PhoneticProperties, text.PhoneticProperties);
522 }
523
528 public override int GetHashCode()
529 {
530 var hashCode = 703246462;
531 foreach (var run in runs)
532 {
533 hashCode = hashCode * -1521134295 + (run?.GetHashCode() ?? 0);
534 }
535 foreach (var phoneticRun in phoeticRuns)
536 {
537 hashCode = hashCode * -1521134295 + (phoneticRun?.GetHashCode() ?? 0);
538 }
539 hashCode = hashCode * -1521134295 + WrapText.GetHashCode();
540 hashCode = hashCode * -1521134295 + (overridePlainText?.GetHashCode() ?? 0);
541 hashCode = hashCode * -1521134295 + (PhoneticProperties?.GetHashCode() ?? 0);
542 return hashCode;
543 }
544
545
546 #endregion
547 }
548}
Builder for creating inline Styles.Font styles with a fluent API.
PhoneticRun.PhoneticType Type
The type of phonetic text representation. Default is FullwidthKatakana.
int FontId
Internal Id of the font used for phonetic text.
PhoneticRun.PhoneticAlignment Alignment
Alignment of the phonetic text relative to the base text. Default is Left.
Represents a phonetic run that provides pronunciation guidance for text.
string Text
The phonetic text to be displayed (Ruby text,like Furigana, Pinyin or Zhuyin).
PhoneticAlignment
Enumeration for phonetic text alignment.
PhoneticType
Enumeration for phonetic text types.
uint StartBase
The start index of the base text (character where the Ruby text starts).
PhoneticRun Copy()
Creates a copy of the current phonetic run.
uint EndBase
The end index of the base text (character where the Ruby text ends).
Represents a single text run with optional inline formatting.
Definition TextRun.cs:18
string Text
Plain text of the run.
Definition TextRun.cs:26
TextRun Copy()
Creates a copy of the current text run.
Definition TextRun.cs:58
Font FontStyle
Font style applied to the text run.
Definition TextRun.cs:40
IReadOnlyList< TextRun > Runs
The list of text runs in this formatted text.
FormattedText AddRun(string text, Action< InlineStyleBuilder > styleBuilder)
Adds a text run using a style builder for inline configuration.
override string ToString()
Gets the string representation of the formatted text without formatting (plain text)....
PhoneticProperties PhoneticProperties
Phonetic properties for the formatted text (Phonetic run / Ruby text).
void Clear()
Clears all runs from this formatted text.
bool WrapText
Gets or sets whether the runs should be rendered with text wrapping, if there are line breaks present...
FormattedText AddPhoneticRun(string text, uint startBase, uint endBase)
Adds a phonetic run for pronunciation guidance (Ruby text, like Furigana, Pinyin or Zhuyin).
static readonly Style LineBreakStyle
Style to be used for line breaks in formatted text.
FormattedText Copy()
Creates a deep copy of this FormattedText instance.
IReadOnlyList< PhoneticRun > PhoneticRuns
The list of phonetic runs (Ruby text) in this formatted text.
string PlainText
Gets the plain text content by concatenating all runs.
override bool Equals(object obj)
Equals method override for comparing two FormattedText instances.
FormattedText AddRun(string text, Font fontStyle=null)
Adds a text run with the specified style.
void AddLineBreak(bool useStyleFromLastRun=true)
Adds a line break to the formatted text. By default, the last run's style is used.
FormattedText SetPhoneticProperties(Font fontReference, PhoneticRun.PhoneticType type=PhoneticRun.PhoneticType.FullwidthKatakana, PhoneticRun.PhoneticAlignment alignment=PhoneticRun.PhoneticAlignment.Left)
Sets the phonetic properties for this formatted text, applied to the phonetic run (Ruby text).
override int GetHashCode()
GetHashCode method override for FormattedText.