TextExtensions.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using QuestPDF.Elements;
  5. using QuestPDF.Elements.Text;
  6. using QuestPDF.Elements.Text.Items;
  7. using QuestPDF.Infrastructure;
  8. using static System.String;
  9. namespace QuestPDF.Fluent
  10. {
  11. public class TextSpanDescriptor
  12. {
  13. internal readonly TextBlockSpan TextBlockSpan;
  14. internal TextSpanDescriptor(TextBlockSpan textBlockSpan)
  15. {
  16. TextBlockSpan = textBlockSpan;
  17. }
  18. internal void MutateTextStyle<T>(Func<TextStyle, T, TextStyle> handler, T argument)
  19. {
  20. TextBlockSpan.Style = handler(TextBlockSpan.Style, argument);
  21. }
  22. internal void MutateTextStyle(Func<TextStyle, TextStyle> handler)
  23. {
  24. TextBlockSpan.Style = handler(TextBlockSpan.Style);
  25. }
  26. }
  27. /// <summary>
  28. /// Transforms a page number into a custom text format (e.g., roman numerals).
  29. /// </summary>
  30. /// <remarks>
  31. /// When <paramref name="pageNumber"/> is null, the delegate should return a default placeholder text of a typical length.
  32. /// </remarks>
  33. public delegate string PageNumberFormatter(int? pageNumber);
  34. public sealed class TextPageNumberDescriptor : TextSpanDescriptor
  35. {
  36. internal Action<PageNumberFormatter> AssignFormatFunction { get; }
  37. internal TextPageNumberDescriptor(TextBlockSpan textBlockSpan, Action<PageNumberFormatter> assignFormatFunction) : base(textBlockSpan)
  38. {
  39. AssignFormatFunction = assignFormatFunction;
  40. AssignFormatFunction(x => x?.ToString());
  41. }
  42. /// <summary>
  43. /// Provides the capability to render the page number in a custom text format (e.g., roman numerals).
  44. /// <a href="https://www.questpdf.com/api-reference/text/page-numbers.html">Lear more</a>
  45. /// </summary>
  46. /// <param name="formatter">The function designated to modify the number into text. When given a null input, a typical-sized placeholder text must be produced.</param>
  47. public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
  48. {
  49. AssignFormatFunction(formatter);
  50. return this;
  51. }
  52. }
  53. public sealed class TextBlockDescriptor : TextSpanDescriptor
  54. {
  55. private TextBlock TextBlock;
  56. internal TextBlockDescriptor(TextBlock textBlock, TextBlockSpan textBlockSpan) : base(textBlockSpan)
  57. {
  58. TextBlock = textBlock;
  59. }
  60. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.left"]/*' />
  61. public TextBlockDescriptor AlignLeft()
  62. {
  63. TextBlock.Alignment = TextHorizontalAlignment.Left;
  64. return this;
  65. }
  66. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.center"]/*' />
  67. public TextBlockDescriptor AlignCenter()
  68. {
  69. TextBlock.Alignment = TextHorizontalAlignment.Center;
  70. return this;
  71. }
  72. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.right"]/*' />
  73. public TextBlockDescriptor AlignRight()
  74. {
  75. TextBlock.Alignment = TextHorizontalAlignment.Right;
  76. return this;
  77. }
  78. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.justify"]/*' />
  79. public TextBlockDescriptor Justify()
  80. {
  81. TextBlock.Alignment = TextHorizontalAlignment.Justify;
  82. return this;
  83. }
  84. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.start"]/*' />
  85. public TextBlockDescriptor AlignStart()
  86. {
  87. TextBlock.Alignment = TextHorizontalAlignment.Start;
  88. return this;
  89. }
  90. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.end"]/*' />
  91. public TextBlockDescriptor AlignEnd()
  92. {
  93. TextBlock.Alignment = TextHorizontalAlignment.End;
  94. return this;
  95. }
  96. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.clampLines"]/*' />
  97. public TextBlockDescriptor ClampLines(int maxLines, string ellipsis = TextDescriptor.DefaultLineClampEllipsis)
  98. {
  99. if (maxLines < 0)
  100. throw new ArgumentException("Line clamp must be greater or equal to zero", nameof(maxLines));
  101. TextBlock.LineClamp = maxLines;
  102. TextBlock.LineClampEllipsis = ellipsis ?? TextDescriptor.DefaultLineClampEllipsis;
  103. return this;
  104. }
  105. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.paragraph.spacing"]/*' />
  106. public TextBlockDescriptor ParagraphSpacing(float value, Unit unit = Unit.Point)
  107. {
  108. if (value < 0)
  109. throw new ArgumentException("Paragraph spacing must be greater or equal to zero", nameof(value));
  110. TextBlock.ParagraphSpacing = value.ToPoints(unit);
  111. return this;
  112. }
  113. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.paragraph.firstLineIndentation"]/*' />
  114. public TextBlockDescriptor ParagraphFirstLineIndentation(float value, Unit unit = Unit.Point)
  115. {
  116. if (value < 0)
  117. throw new ArgumentException("Paragraph indentation must be greater or equal to zero", nameof(value));
  118. TextBlock.ParagraphFirstLineIndentation = value.ToPoints(unit);
  119. return this;
  120. }
  121. }
  122. public sealed class TextDescriptor
  123. {
  124. internal TextBlock TextBlock { get; } = new();
  125. private TextStyle? DefaultStyle { get; set; }
  126. internal const string DefaultLineClampEllipsis = "…";
  127. /// <summary>
  128. /// Applies a consistent text style for the whole content within this <see cref="TextExtensions.Text">Text</see> element.
  129. /// </summary>
  130. /// <param name="style">The TextStyle object to override the default inherited text style.</param>
  131. public void DefaultTextStyle(TextStyle style)
  132. {
  133. DefaultStyle = style;
  134. }
  135. /// <summary>
  136. /// Applies a consistent text style for the whole content within this <see cref="TextExtensions.Text">Text</see> element.
  137. /// </summary>
  138. /// <param name="handler">Handler to modify the default inherited text style.</param>
  139. public void DefaultTextStyle(Func<TextStyle, TextStyle> style)
  140. {
  141. DefaultStyle = style(TextStyle.Default);
  142. }
  143. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.left"]/*' />
  144. public void AlignLeft()
  145. {
  146. TextBlock.Alignment = TextHorizontalAlignment.Left;
  147. }
  148. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.center"]/*' />
  149. public void AlignCenter()
  150. {
  151. TextBlock.Alignment = TextHorizontalAlignment.Center;
  152. }
  153. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.right"]/*' />
  154. public void AlignRight()
  155. {
  156. TextBlock.Alignment = TextHorizontalAlignment.Right;
  157. }
  158. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.justify"]/*' />
  159. public void Justify()
  160. {
  161. TextBlock.Alignment = TextHorizontalAlignment.Justify;
  162. }
  163. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.start"]/*' />
  164. public void AlignStart()
  165. {
  166. TextBlock.Alignment = TextHorizontalAlignment.Start;
  167. }
  168. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.alignment.end"]/*' />
  169. public void AlignEnd()
  170. {
  171. TextBlock.Alignment = TextHorizontalAlignment.End;
  172. }
  173. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.clampLines"]/*' />
  174. public void ClampLines(int maxLines, string ellipsis = DefaultLineClampEllipsis)
  175. {
  176. TextBlock.LineClamp = maxLines;
  177. TextBlock.LineClampEllipsis = ellipsis;
  178. }
  179. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.paragraph.spacing"]/*' />
  180. public void ParagraphSpacing(float value, Unit unit = Unit.Point)
  181. {
  182. if (value < 0)
  183. throw new ArgumentException("Paragraph spacing must be greater or equal to zero", nameof(value));
  184. TextBlock.ParagraphSpacing = value.ToPoints(unit);
  185. }
  186. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.paragraph.firstLineIndentation"]/*' />
  187. public void ParagraphFirstLineIndentation(float value, Unit unit = Unit.Point)
  188. {
  189. if (value < 0)
  190. throw new ArgumentException("Paragraph indentation must be greater or equal to zero", nameof(value));
  191. TextBlock.ParagraphFirstLineIndentation = value.ToPoints(unit);
  192. }
  193. [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
  194. public void Span(string? text, TextStyle style)
  195. {
  196. Span(text).Style(style);
  197. }
  198. /// <summary>
  199. /// Appends the given text to the current paragraph.
  200. /// </summary>
  201. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.spanDescriptor"]/*' />
  202. public TextSpanDescriptor Span(string? text)
  203. {
  204. if (text == null)
  205. return new TextSpanDescriptor(new TextBlockSpan());
  206. var textSpan = new TextBlockSpan() { Text = text };
  207. TextBlock.Items.Add(textSpan);
  208. return new TextSpanDescriptor(textSpan);
  209. }
  210. /// <summary>
  211. /// Appends a line with the provided text followed by an environment-specific newline character.
  212. /// </summary>
  213. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.spanDescriptor"]/*' />
  214. public TextSpanDescriptor Line(string? text)
  215. {
  216. text ??= string.Empty;
  217. return Span(text + "\n");
  218. }
  219. /// <summary>
  220. /// Appends a blank line.
  221. /// </summary>
  222. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.spanDescriptor"]/*' />
  223. public TextSpanDescriptor EmptyLine()
  224. {
  225. return Span("\n");
  226. }
  227. private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
  228. {
  229. var textBlockItem = new TextBlockPageNumber();
  230. TextBlock.Items.Add(textBlockItem);
  231. return new TextPageNumberDescriptor(textBlockItem, x => textBlockItem.Source = context => x(pageNumber(context)));
  232. }
  233. /// <summary>
  234. /// Appends text showing the current page number.
  235. /// </summary>
  236. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.pageNumberDescriptor"]/*' />
  237. public TextPageNumberDescriptor CurrentPageNumber()
  238. {
  239. return PageNumber(x => x.CurrentPage);
  240. }
  241. /// <summary>
  242. /// Appends text showing the total number of pages in the document.
  243. /// </summary>
  244. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.pageNumberDescriptor"]/*' />
  245. public TextPageNumberDescriptor TotalPages()
  246. {
  247. return PageNumber(x => x.DocumentLength);
  248. }
  249. [Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")]
  250. public void PageNumberOfLocation(string sectionName, TextStyle? style = null)
  251. {
  252. BeginPageNumberOfSection(sectionName).Style(style);
  253. }
  254. /// <summary>
  255. /// Appends text showing the number of the first page of the specified <see cref="ElementExtensions.Section">Section</see>.
  256. /// </summary>
  257. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="param.sectionName"]/*' />
  258. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.pageNumberDescriptor"]/*' />
  259. public TextPageNumberDescriptor BeginPageNumberOfSection(string sectionName)
  260. {
  261. return PageNumber(x => x.GetLocation(sectionName)?.PageStart);
  262. }
  263. /// <summary>
  264. /// Appends text showing the number of the last page of the specified <see cref="ElementExtensions.Section">Section</see>.
  265. /// </summary>
  266. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="param.sectionName"]/*' />
  267. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.pageNumberDescriptor"]/*' />
  268. public TextPageNumberDescriptor EndPageNumberOfSection(string sectionName)
  269. {
  270. return PageNumber(x => x.GetLocation(sectionName)?.PageEnd);
  271. }
  272. /// <summary>
  273. /// Appends text showing the page number relative to the beginning of the given <see cref="ElementExtensions.Section">Section</see>.
  274. /// </summary>
  275. /// <example>
  276. /// For a section spanning pages 20 to 50, page 35 will show as 15.
  277. /// </example>
  278. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="param.sectionName"]/*' />
  279. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.pageNumberDescriptor"]/*' />
  280. public TextPageNumberDescriptor PageNumberWithinSection(string sectionName)
  281. {
  282. return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(sectionName)?.PageStart);
  283. }
  284. /// <summary>
  285. /// Appends text showing the total number of pages within the given <see cref="ElementExtensions.Section">Section</see>.
  286. /// </summary>
  287. /// <example>
  288. /// For a section spanning pages 20 to 50, the total is 30 pages.
  289. /// </example>
  290. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="param.sectionName"]/*' />
  291. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.pageNumberDescriptor"]/*' />
  292. public TextPageNumberDescriptor TotalPagesWithinSection(string sectionName)
  293. {
  294. return PageNumber(x => x.GetLocation(sectionName)?.Length);
  295. }
  296. /// <summary>
  297. /// Creates a clickable text that navigates the user to a specified <see cref="ElementExtensions.Section">Section</see>.
  298. /// </summary>
  299. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="param.sectionName"]/*' />
  300. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.spanDescriptor"]/*' />
  301. public TextSpanDescriptor SectionLink(string? text, string sectionName)
  302. {
  303. if (IsNullOrEmpty(sectionName))
  304. throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
  305. if (text == null)
  306. return new TextSpanDescriptor(new TextBlockSpan());
  307. var textBlockItem = new TextBlockSectionLink
  308. {
  309. Text = text,
  310. SectionName = sectionName
  311. };
  312. TextBlock.Items.Add(textBlockItem);
  313. return new TextSpanDescriptor(textBlockItem);
  314. }
  315. [Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
  316. public void InternalLocation(string? text, string locationName, TextStyle? style = null)
  317. {
  318. SectionLink(text, locationName).Style(style);
  319. }
  320. /// <summary>
  321. /// Creates a clickable text that redirects the user to a specific webpage.
  322. /// </summary>
  323. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="param.url"]/*' />
  324. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.spanDescriptor"]/*' />
  325. public TextSpanDescriptor Hyperlink(string? text, string url)
  326. {
  327. if (IsNullOrEmpty(url))
  328. throw new ArgumentException("Url cannot be null or empty", nameof(url));
  329. if (text == null)
  330. return new TextSpanDescriptor(new TextBlockSpan());
  331. var textBlockItem = new TextBlockHyperlink
  332. {
  333. Text = text,
  334. Url = url
  335. };
  336. TextBlock.Items.Add(textBlockItem);
  337. return new TextSpanDescriptor(textBlockItem);
  338. }
  339. [Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
  340. public void ExternalLocation(string? text, string url, TextStyle? style = null)
  341. {
  342. Hyperlink(text, url).Style(style);
  343. }
  344. /// <summary>
  345. /// Embeds custom content within the text.
  346. /// </summary>
  347. /// <remarks>
  348. /// The container must fit within one line and can not span multiple pages.
  349. /// </remarks>
  350. /// <param name="alignment">Defines the position of the injected element in relation to text typography features (baseline, top/bottom edge).</param>
  351. /// <returns>A container for the embedded content. Populate using the Fluent API.</returns>
  352. public IContainer Element(TextInjectedElementAlignment alignment = TextInjectedElementAlignment.AboveBaseline)
  353. {
  354. var container = new Container();
  355. TextBlock.Items.Add(new TextBlockElement
  356. {
  357. Element = container,
  358. Alignment = alignment
  359. });
  360. return container.AlignBottom().MinimalBox();
  361. }
  362. /// <summary>
  363. /// Embeds custom content within the text.
  364. /// </summary>
  365. /// <remarks>
  366. /// The container must fit within one line and can not span multiple pages.
  367. /// </remarks>
  368. /// <param name="alignment">Defines the position of the injected element in relation to text typography features (baseline, top/bottom edge).</param>
  369. /// <param name="handler">Delegate to populate the embedded container with custom content.</param>
  370. public void Element(Action<IContainer> handler, TextInjectedElementAlignment alignment = TextInjectedElementAlignment.AboveBaseline)
  371. {
  372. handler(Element(alignment));
  373. }
  374. internal void Compose(IContainer container)
  375. {
  376. if (DefaultStyle != null)
  377. container = container.DefaultTextStyle(DefaultStyle);
  378. container.Element(TextBlock);
  379. }
  380. }
  381. public static class TextExtensions
  382. {
  383. /// <summary>
  384. /// Draws rich formatted text.
  385. /// </summary>
  386. /// <param name="content">Handler to define the content of the text elements (e.g.: paragraphs, spans, hyperlinks, page numbers).</param>
  387. public static void Text(this IContainer element, Action<TextDescriptor> content)
  388. {
  389. var descriptor = new TextDescriptor();
  390. if (element is Alignment alignment)
  391. descriptor.TextBlock.Alignment = MapAlignment(alignment.Horizontal);
  392. content?.Invoke(descriptor);
  393. descriptor.Compose(element);
  394. }
  395. [Obsolete("This method has been deprecated since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
  396. public static void Text(this IContainer element, object? text, TextStyle style)
  397. {
  398. element.Text(text).Style(style);
  399. }
  400. [Obsolete("This method has been deprecated since version 2022.12. Please use an overload where the text parameter is passed explicitly as a string.")]
  401. public static TextSpanDescriptor Text(this IContainer element, object? text)
  402. {
  403. return element.Text(text?.ToString());
  404. }
  405. /// <summary>
  406. /// Draws the provided text on the page
  407. /// </summary>
  408. /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.returns.spanDescriptor"]/*' />
  409. public static TextBlockDescriptor Text(this IContainer container, string? text)
  410. {
  411. if (text == null)
  412. return new TextBlockDescriptor(new TextBlock(), new TextBlockSpan());
  413. var textBlock = new TextBlock();
  414. container.Element(textBlock);
  415. if (container is Alignment alignment)
  416. textBlock.Alignment = MapAlignment(alignment.Horizontal);
  417. var textSpan = new TextBlockSpan { Text = text };
  418. textBlock.Items.Add(textSpan);
  419. return new TextBlockDescriptor(textBlock, textSpan);
  420. }
  421. private static TextHorizontalAlignment? MapAlignment(HorizontalAlignment? alignment)
  422. {
  423. return alignment switch
  424. {
  425. HorizontalAlignment.Left => TextHorizontalAlignment.Left,
  426. HorizontalAlignment.Center => TextHorizontalAlignment.Center,
  427. HorizontalAlignment.Right => TextHorizontalAlignment.Right,
  428. _ => null
  429. };
  430. }
  431. }
  432. }