/sources/TemplateEngine.Docx/Processors/ListProcessor.cs

https://bitbucket.org/unit6ru/templateengine · C# · 348 lines · 255 code · 57 blank · 36 comment · 33 complexity · 8e7024c3c69aa27d884b5c20d5070a68 MD5 · raw file

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Xml.Linq;
  5. namespace TemplateEngine.Docx.Processors
  6. {
  7. internal class ListProcessor:IProcessor
  8. {
  9. private bool _isNeedToRemoveContentControls;
  10. private readonly ProcessContext _context;
  11. private class PropagationProcessResult : ProcessResult
  12. {
  13. internal IEnumerable<XElement> Result { get; set; }
  14. }
  15. /// <summary>
  16. /// Entire list prototype, includes all list levels.
  17. /// </summary>
  18. private class Prototype
  19. {
  20. private readonly ProcessContext _context;
  21. /// <summary>
  22. /// Creates prototype from list of prototype items.
  23. /// </summary>
  24. /// <param name="context">Process context.</param>
  25. /// <param name="prototypeItems">List of prototype items.</param>
  26. private Prototype(ProcessContext context, IEnumerable<XElement> prototypeItems)
  27. {
  28. _context = context;
  29. PrototypeItems = prototypeItems.ToList();
  30. }
  31. /// <summary>
  32. /// Creates prototype from list content control and fieldNames.
  33. /// </summary>
  34. /// <param name="context">Process context.</param>
  35. /// <param name="listContentControl">List content control element.</param>
  36. /// <param name="fieldNames">Names of fields with content.</param>
  37. public Prototype(ProcessContext context, XElement listContentControl, IEnumerable<string> fieldNames)
  38. {
  39. _context = context;
  40. if (listContentControl.Name != W.sdt)
  41. throw new Exception("List content control is not a content control element");
  42. fieldNames = fieldNames.ToList();
  43. // All elements inside list control content are included to the prototype.
  44. var listItems = listContentControl
  45. .Element(W.sdtContent)
  46. .Elements()
  47. .ToList();
  48. var tagsInPrototype = listItems.DescendantsAndSelf(W.sdt)
  49. .Select(sdt => sdt.SdtTagName());
  50. // If any field not found return empty list.
  51. if (fieldNames.Any(fn => !tagsInPrototype.Contains(fn)))
  52. {
  53. IsValid = false;
  54. return;
  55. }
  56. IsValid = true;
  57. PrototypeItems = listItems;
  58. }
  59. public bool IsValid { get; private set; }
  60. public List<XElement> PrototypeItems { get; private set; }
  61. public Prototype Exclude(LevelPrototype prototypeForExclude)
  62. {
  63. return new Prototype(_context, PrototypeItems
  64. .Where(itemPrototype => !prototypeForExclude
  65. .PrototypeItems
  66. .Contains(itemPrototype)));
  67. }
  68. /// <summary>
  69. /// Retrieves prototype for current content item.
  70. /// </summary>
  71. /// <param name="fieldNames">Fields names for current content item.</param>
  72. /// <returns>Prototype for current content item.</returns>
  73. public LevelPrototype CurrentLevelPrototype(IEnumerable<string> fieldNames)
  74. {
  75. return new LevelPrototype(_context, PrototypeItems, fieldNames);
  76. }
  77. }
  78. /// <summary>
  79. /// Prototype of the concrete level of list.
  80. /// </summary>
  81. private class LevelPrototype
  82. {
  83. private readonly ProcessContext _context;
  84. public bool IsValid { get; private set; }
  85. public LevelPrototype(ProcessContext context, IEnumerable<XElement> prototypeItems, IEnumerable<string> fieldNames)
  86. {
  87. _context = context;
  88. var currentLevelPrototype = new List<XElement>();
  89. // Items for which no content control.
  90. // Add this items to the prototype if there are items after them.
  91. var maybeNeedToAdd = new List<XElement>();
  92. var numberingElementReached = false;
  93. foreach (var prototypeItem in prototypeItems)
  94. {
  95. //search for first item with numbering
  96. if (!numberingElementReached)
  97. {
  98. var paragraph = prototypeItem.DescendantsAndSelf(W.p).FirstOrDefault();
  99. if (paragraph != null &&
  100. ListItemRetriever.RetrieveListItem(
  101. context.NumberingPart, context.StylesPart, paragraph)
  102. .IsListItem)
  103. numberingElementReached = true;
  104. else
  105. continue;
  106. }
  107. if ((!prototypeItem.FirstLevelDescendantsAndSelf(W.sdt).Any() && prototypeItem.Value != "") ||
  108. (prototypeItem
  109. .FirstLevelDescendantsAndSelf(W.sdt)
  110. .Any(sdt => fieldNames.Contains(sdt.SdtTagName()))))
  111. {
  112. currentLevelPrototype.AddRange(maybeNeedToAdd);
  113. currentLevelPrototype.Add(prototypeItem);
  114. }
  115. else
  116. {
  117. maybeNeedToAdd.Add(prototypeItem);
  118. }
  119. }
  120. if (!currentLevelPrototype.Any()) return;
  121. PrototypeItems = currentLevelPrototype;
  122. if (fieldNames.Any(fn => !SdtTags.Contains(fn)))
  123. {
  124. IsValid = false;
  125. return;
  126. }
  127. IsValid = true;
  128. PrototypeItems = currentLevelPrototype;
  129. }
  130. private LevelPrototype(ProcessContext context, IEnumerable<XElement> prototypeItems)
  131. {
  132. _context = context;
  133. PrototypeItems = prototypeItems.ToList();
  134. }
  135. public List<XElement> PrototypeItems { get; private set; }
  136. private List<string> SdtTags
  137. {
  138. get
  139. {
  140. return PrototypeItems == null
  141. ? new List<string>()
  142. : PrototypeItems.DescendantsAndSelf(W.sdt)
  143. .Select(sdt => sdt.SdtTagName())
  144. .ToList();
  145. }
  146. }
  147. public LevelPrototype Clone()
  148. {
  149. return new LevelPrototype(_context, PrototypeItems.ToList());
  150. }
  151. }
  152. public ListProcessor(ProcessContext context)
  153. {
  154. _context = context;
  155. }
  156. public IProcessor SetRemoveContentControls(bool isNeedToRemove)
  157. {
  158. _isNeedToRemoveContentControls = isNeedToRemove;
  159. return this;
  160. }
  161. public ProcessResult FillContent(XElement contentControl, IEnumerable<IContentItem> items)
  162. {
  163. var processResult = new ProcessResult();
  164. var handled = false;
  165. foreach (var contentItem in items)
  166. {
  167. var itemProcessResult = FillContent(contentControl, contentItem);
  168. if (!itemProcessResult.Handled) continue;
  169. handled = true;
  170. if (!itemProcessResult.Success)
  171. processResult.Errors.AddRange(itemProcessResult.Errors);
  172. }
  173. if (!handled) return ProcessResult.NotHandledResult;
  174. if (processResult.Success && _isNeedToRemoveContentControls)
  175. {
  176. foreach (var sdt in contentControl.Descendants(W.sdt).ToList())
  177. {
  178. // Remove the content control, and replace it with its contents.
  179. sdt.RemoveContentControl();
  180. }
  181. contentControl.RemoveContentControl();
  182. }
  183. return processResult;
  184. }
  185. private ProcessResult FillContent(XElement contentControl, IContentItem item)
  186. {
  187. var processResult = new ProcessResult();
  188. if (!(item is ListContent))
  189. {
  190. return ProcessResult.NotHandledResult;
  191. }
  192. var list = item as ListContent;
  193. var listName = list.Name;
  194. // If there isn't a list with that name, add an error to the error string.
  195. if (contentControl == null)
  196. {
  197. processResult.Errors.Add(String.Format("List Content Control '{0}' not found.",
  198. listName));
  199. return processResult;
  200. }
  201. // If the list doesn't contain content controls in items, then error.
  202. var itemsContentControl = contentControl
  203. .Descendants(W.sdt)
  204. .FirstOrDefault();
  205. if (itemsContentControl == null)
  206. {
  207. processResult.Errors.Add(String.Format(
  208. "List Content Control '{0}' doesn't contain content controls in items.",
  209. listName));
  210. return processResult;
  211. }
  212. var fieldNames = list.FieldNames.ToList();
  213. // Create a prototype of new items to be inserted into the document.
  214. var prototype = new Prototype(_context, contentControl, fieldNames);
  215. if (!prototype.IsValid)
  216. {
  217. processResult.Errors.Add(String.Format(
  218. "List Content Control '{0}' doesn't contain items with content controls {1}.",
  219. listName,
  220. string.Join(", ", fieldNames)));
  221. return processResult;
  222. }
  223. new NumberingAccessor(_context.NumberingPart, _context.LastNumIds)
  224. .ResetNumbering(prototype.PrototypeItems);
  225. // Propagates a prototype.
  226. var propagationResult = PropagatePrototype(prototype, list.Items);
  227. if (!propagationResult.Success)
  228. {
  229. processResult.Errors.AddRange(propagationResult.Errors);
  230. }
  231. // Remove the prototype row and add all of the newly constructed rows.
  232. prototype.PrototypeItems.Last().AddAfterSelf(propagationResult.Result);
  233. prototype.PrototypeItems.Remove();
  234. return processResult;
  235. }
  236. // Fills prototype with values recursive.
  237. private PropagationProcessResult PropagatePrototype(Prototype prototype,
  238. IEnumerable<ListItemContent> content)
  239. {
  240. var processResult = new PropagationProcessResult();
  241. var newRows = new List<XElement>();
  242. foreach (var contentItem in content)
  243. {
  244. var currentLevelPrototype = prototype.CurrentLevelPrototype(contentItem.FieldNames);
  245. if (currentLevelPrototype == null || !currentLevelPrototype.IsValid)
  246. {
  247. processResult.Errors.Add(
  248. string.Format("Prototype for list item '{0}' not found",
  249. string.Join(", ", contentItem.FieldNames)));
  250. continue;
  251. }
  252. // Create new item from the prototype.
  253. var newItemEntry = currentLevelPrototype.Clone();
  254. foreach (var xElement in newItemEntry.PrototypeItems)
  255. {
  256. var newElement = new XElement(xElement);
  257. if (!newElement.DescendantsAndSelf(W.sdt).Any())
  258. {
  259. newRows.Add(newElement);
  260. continue;
  261. }
  262. foreach (var sdt in newElement.FirstLevelDescendantsAndSelf(W.sdt).ToList())
  263. {
  264. var fieldContent = contentItem.GetContentItem(sdt.SdtTagName());
  265. if (fieldContent == null)
  266. {
  267. processResult.Errors.Add(string.Format("Field content for field '{0}' not found", sdt.SdtTagName()));
  268. continue;
  269. }
  270. var contentProcessResult = new ContentProcessor(_context)
  271. .SetRemoveContentControls(_isNeedToRemoveContentControls)
  272. .FillContent(sdt, fieldContent);
  273. if (!contentProcessResult.Success)
  274. processResult.Errors.AddRange(processResult.Errors);
  275. }
  276. newRows.Add(newElement);
  277. }
  278. // If there are nested items fill prototype for them.
  279. if (contentItem.NestedFields != null)
  280. {
  281. var filledNestedFields = PropagatePrototype(
  282. prototype.Exclude(currentLevelPrototype),
  283. contentItem.NestedFields);
  284. newRows.AddRange(filledNestedFields.Result);
  285. }
  286. }
  287. processResult.Result = newRows;
  288. return processResult;
  289. }
  290. }
  291. }