| /*************************************************************************** |
| |
| Copyright (c) Microsoft Corporation 2012-2015. |
| |
| This code is licensed using the Microsoft Public License (Ms-PL). The text of the license can be found here: |
| |
| http://www.microsoft.com/resources/sharedsource/licensingbasics/publiclicense.mspx |
| |
| Published at http://OpenXmlDeveloper.org |
| Resource Center and Documentation: http://openxmldeveloper.org/wiki/w/wiki/powertools-for-open-xml.aspx |
| |
| Developer: Eric White |
| Blog: http://www.ericwhite.com |
| Twitter: @EricWhiteDev |
| Email: eric@ericwhite.com |
| |
| Version: 2.7.00 |
| * Complete re-write - new architecture enables much more accurate rendering of list items. |
| |
| Version: 2.6.03 |
| * Fixed: Empty paragraphs were not counted properly |
| * Fixed: Numbered styles were not processed properly if they derived from another style |
| * Now has a dependency on FormattingAssembler.cs |
| |
| Version: 2.6.00 |
| * Enhancements to support HtmlConverter.cs |
| |
| Relevant Screen-Casts |
| * https://www.youtube.com/watch?v=w9h1VQ3eR_Q |
| |
| ***************************************************************************/ |
| |
| using System; |
| using System.Collections.Generic; |
| using System.Linq; |
| using System.Text; |
| using System.Xml.Linq; |
| using DocumentFormat.OpenXml.Packaging; |
| |
| namespace OpenXmlPowerTools |
| { |
| public class ListItemRetrieverSettings |
| { |
| public static Dictionary<string, Func<string, int, string, string>> DefaultListItemTextImplementations = |
| new Dictionary<string, Func<string, int, string, string>>() |
| { |
| {"fr-FR", ListItemTextGetter_fr_FR.GetListItemText}, |
| {"tr-TR", ListItemTextGetter_tr_TR.GetListItemText}, |
| {"ru-RU", ListItemTextGetter_ru_RU.GetListItemText}, |
| {"sv-SE", ListItemTextGetter_sv_SE.GetListItemText}, |
| {"zh-CN", ListItemTextGetter_zh_CN.GetListItemText}, |
| }; |
| public Dictionary<string, Func<string, int, string, string>> ListItemTextImplementations; |
| public ListItemRetrieverSettings() |
| { |
| ListItemTextImplementations = DefaultListItemTextImplementations; |
| } |
| } |
| |
| public class ListItemRetriever |
| { |
| public class ListItemSourceSet |
| { |
| public int NumId; // numId from the paragraph or style |
| public XElement Num; // num element from the numbering part |
| public int AbstractNumId; // abstract numId |
| public XElement AbstractNum; // abstractNum element |
| |
| public ListItemSourceSet(XDocument numXDoc, XDocument styleXDoc, int numId) |
| { |
| NumId = numId; |
| |
| Num = numXDoc |
| .Root |
| .Elements(W.num) |
| .FirstOrDefault(n => (int)n.Attribute(W.numId) == numId); |
| |
| AbstractNumId = (int)Num |
| .Elements(W.abstractNumId) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| AbstractNum = numXDoc |
| .Root |
| .Elements(W.abstractNum) |
| .Where(e => (int)e.Attribute(W.abstractNumId) == AbstractNumId) |
| .FirstOrDefault(); |
| } |
| |
| public int? StartOverride(int ilvl) |
| { |
| var lvlOverride = Num |
| .Elements(W.lvlOverride) |
| .FirstOrDefault(nlo => (int)nlo.Attribute(W.ilvl) == ilvl); |
| if (lvlOverride != null) |
| return (int?)lvlOverride |
| .Elements(W.startOverride) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| return null; |
| } |
| |
| public XElement OverrideLvl(int ilvl) |
| { |
| var lvlOverride = Num |
| .Elements(W.lvlOverride) |
| .FirstOrDefault(nlo => (int)nlo.Attribute(W.ilvl) == ilvl); |
| if (lvlOverride != null) |
| return lvlOverride.Element(W.lvl); |
| return null; |
| } |
| |
| public XElement AbstractLvl(int ilvl) |
| { |
| return AbstractNum |
| .Elements(W.lvl) |
| .FirstOrDefault(al => (int)al.Attribute(W.ilvl) == ilvl); |
| } |
| |
| public XElement Lvl(int ilvl) |
| { |
| var overrideLvl = OverrideLvl(ilvl); |
| if (overrideLvl != null) |
| return overrideLvl; |
| return AbstractLvl(ilvl); |
| } |
| } |
| |
| public class ListItemSource |
| { |
| public ListItemSourceSet Main; |
| public string NumStyleLinkName; |
| public ListItemSourceSet NumStyleLink; |
| public int Style_ilvl; |
| |
| // for list item sources that use numStyleLink, there are two abstractId values. |
| // The abstractId that is use is in num->abstractNum->numStyleLink->style->num->abstractNum |
| |
| public ListItemSource(XDocument numXDoc, XDocument stylesXDoc, int numId) |
| { |
| Main = new ListItemSourceSet(numXDoc, stylesXDoc, numId); |
| |
| NumStyleLinkName = (string)Main |
| .AbstractNum |
| .Elements(W.numStyleLink) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (NumStyleLinkName != null) |
| { |
| var numStyleLinkNumId = (int?)stylesXDoc |
| .Root |
| .Elements(W.style) |
| .Where(s => (string)s.Attribute(W.styleId) == NumStyleLinkName) |
| .Elements(W.pPr) |
| .Elements(W.numPr) |
| .Elements(W.numId) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (numStyleLinkNumId != null) |
| NumStyleLink = new ListItemSourceSet(numXDoc, stylesXDoc, (int)numStyleLinkNumId); |
| } |
| } |
| |
| public XElement Lvl(int ilvl) |
| { |
| if (NumStyleLink != null) |
| { |
| var lvl = NumStyleLink.Lvl(ilvl); |
| if (lvl == null) |
| { |
| for (int i = ilvl - 1; i >= 0; i--) |
| { |
| lvl = NumStyleLink.Lvl(i); |
| if (lvl != null) |
| break; |
| } |
| } |
| return lvl; |
| } |
| var lvl2 = Main.Lvl(ilvl); |
| if (lvl2 == null) |
| { |
| for (int i = ilvl - 1; i >= 0; i--) |
| { |
| lvl2 = Main.Lvl(i); |
| if (lvl2 != null) |
| break; |
| } |
| } |
| return lvl2; |
| } |
| |
| public int? StartOverride(int ilvl) |
| { |
| if (NumStyleLink != null) |
| { |
| var startOverride = NumStyleLink.StartOverride(ilvl); |
| if (startOverride != null) |
| return startOverride; |
| } |
| return Main.StartOverride(ilvl); |
| } |
| |
| public int Start(int ilvl) |
| { |
| var lvl = Lvl(ilvl); |
| var start = (int?)lvl.Elements(W.start).Attributes(W.val).FirstOrDefault(); |
| if (start != null) |
| return (int)start; |
| return 0; |
| } |
| |
| public int AbstractNumId |
| { |
| get |
| { |
| return Main.AbstractNumId; |
| } |
| } |
| } |
| |
| public class ListItemInfo |
| { |
| public bool IsListItem; |
| public bool IsZeroNumId; |
| |
| public ListItemSource FromStyle; |
| public ListItemSource FromParagraph; |
| |
| private int? mAbstractNumId = null; |
| |
| public int? AbstractNumId |
| { |
| get |
| { |
| // note: this property does not get NumStyleLinkAbstractNumId |
| // it presumes that we are only interested in AbstractNumId |
| // however, it is easy enough to change if necessary |
| |
| if (mAbstractNumId != null) |
| return mAbstractNumId; |
| if (FromParagraph != null) |
| mAbstractNumId = FromParagraph.AbstractNumId; |
| else if (FromStyle != null) |
| mAbstractNumId = FromStyle.AbstractNumId; |
| return mAbstractNumId; |
| } |
| } |
| |
| public XElement Lvl(int ilvl) |
| { |
| if (FromParagraph != null) |
| { |
| var lvl = FromParagraph.Lvl(ilvl); |
| if (lvl == null) |
| { |
| for (int i = ilvl - 1; i >= 0; i--) |
| { |
| lvl = FromParagraph.Lvl(i); |
| if (lvl != null) |
| break; |
| } |
| } |
| return lvl; |
| } |
| var lvl2 = FromStyle.Lvl(ilvl); |
| if (lvl2 == null) |
| { |
| for (int i = ilvl - 1; i >= 0; i--) |
| { |
| lvl2 = FromParagraph.Lvl(i); |
| if (lvl2 != null) |
| break; |
| } |
| } |
| return lvl2; |
| } |
| |
| public int Start(int ilvl) |
| { |
| if (FromParagraph != null) |
| return FromParagraph.Start(ilvl); |
| return FromStyle.Start(ilvl); |
| } |
| |
| public int Start(int ilvl, bool takeOverride, out bool isOverride) |
| { |
| if (FromParagraph != null) |
| { |
| if (takeOverride) |
| { |
| var startOverride = FromParagraph.StartOverride(ilvl); |
| if (startOverride != null) |
| { |
| isOverride = true; |
| return (int)startOverride; |
| } |
| } |
| isOverride = false; |
| return FromParagraph.Start(ilvl); |
| } |
| else if (this.FromStyle != null) |
| { |
| if (takeOverride) |
| { |
| var startOverride = FromStyle.StartOverride(ilvl); |
| if (startOverride != null) |
| { |
| isOverride = true; |
| return (int)startOverride; |
| } |
| } |
| isOverride = false; |
| return FromStyle.Start(ilvl); |
| } |
| isOverride = false; |
| return 0; |
| } |
| |
| public int? StartOverride(int ilvl) |
| { |
| if (FromParagraph != null) |
| { |
| var startOverride = FromParagraph.StartOverride(ilvl); |
| if (startOverride != null) |
| return (int)startOverride; |
| return null; |
| } |
| else if (this.FromStyle != null) |
| { |
| var startOverride = FromStyle.StartOverride(ilvl); |
| if (startOverride != null) |
| return (int)startOverride; |
| return null; |
| } |
| return null; |
| } |
| |
| private int? mNumId; |
| |
| public int NumId |
| { |
| get |
| { |
| if (mNumId != null) |
| return (int)mNumId; |
| if (FromParagraph != null) |
| mNumId = FromParagraph.Main.NumId; |
| else if (FromStyle != null) |
| mNumId = FromStyle.Main.NumId; |
| return (int)mNumId; |
| } |
| } |
| |
| public ListItemInfo() { } |
| |
| public ListItemInfo(bool isListItem, bool isZeroNumId) |
| { |
| IsListItem = isListItem; |
| IsZeroNumId = isZeroNumId; |
| } |
| } |
| |
| public static void SetParagraphLevel(XElement paragraph, int ilvl) |
| { |
| var pi = paragraph.Annotation<ParagraphInfo>(); |
| if (pi == null) |
| { |
| pi = new ParagraphInfo() |
| { |
| Ilvl = ilvl, |
| }; |
| paragraph.AddAnnotation(pi); |
| return; |
| } |
| throw new OpenXmlPowerToolsException("Internal error - should never set ilvl more than once."); |
| } |
| |
| public static int GetParagraphLevel(XElement paragraph) |
| { |
| var pi = paragraph.Annotation<ParagraphInfo>(); |
| if (pi != null) |
| return pi.Ilvl; |
| throw new OpenXmlPowerToolsException("Internal error - should never ask for ilvl without it first being set."); |
| } |
| |
| public static ListItemInfo GetListItemInfo(XDocument numXDoc, XDocument stylesXDoc, XElement paragraph) |
| { |
| // The following is an optimization - only determine ListItemInfo once for a |
| // paragraph. |
| ListItemInfo listItemInfo = paragraph.Annotation<ListItemInfo>(); |
| if (listItemInfo != null) |
| return listItemInfo; |
| throw new OpenXmlPowerToolsException("Attempting to retrieve ListItemInfo before initialization"); |
| } |
| |
| private static ListItemInfo NotAListItem = new ListItemInfo(false, true); |
| private static ListItemInfo ZeroNumId = new ListItemInfo(false, false); |
| |
| public static void InitListItemInfo(XDocument numXDoc, XDocument stylesXDoc, XElement paragraph) |
| { |
| if (FirstRunIsEmptySectionBreak(paragraph)) |
| { |
| paragraph.AddAnnotation(NotAListItem); |
| return; |
| } |
| |
| int? paragraphNumId = null; |
| |
| XElement paragraphNumberingProperties = paragraph |
| .Elements(W.pPr) |
| .Elements(W.numPr) |
| .FirstOrDefault(); |
| |
| if (paragraphNumberingProperties != null) |
| { |
| paragraphNumId = (int?)paragraphNumberingProperties |
| .Elements(W.numId) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| // if numPr of paragraph does not contain numId, then it is not a list item. |
| // if numId of paragraph == 0, then this is not a list item, regardless of the markup in the style. |
| if (paragraphNumId == null || paragraphNumId == 0) |
| { |
| paragraph.AddAnnotation(NotAListItem); |
| return; |
| } |
| } |
| |
| string paragraphStyleName = GetParagraphStyleName(stylesXDoc, paragraph); |
| |
| var listItemInfo = GetListItemInfoFromCache(numXDoc, paragraphStyleName, paragraphNumId); |
| if (listItemInfo != null) |
| { |
| paragraph.AddAnnotation(listItemInfo); |
| |
| if (listItemInfo.FromParagraph != null) |
| { |
| var para_ilvl = (int?)paragraphNumberingProperties |
| .Elements(W.ilvl) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (para_ilvl == null) |
| para_ilvl = 0; |
| |
| var abstractNum = listItemInfo.FromParagraph.Main.AbstractNum; |
| var multiLevelType = (string)abstractNum.Elements(W.multiLevelType).Attributes(W.val).FirstOrDefault(); |
| if (multiLevelType == "singleLevel") |
| para_ilvl = 0; |
| |
| SetParagraphLevel(paragraph, (int)para_ilvl); |
| } |
| else if (listItemInfo.FromStyle != null) |
| { |
| int this_ilvl = listItemInfo.FromStyle.Style_ilvl; |
| var abstractNum = listItemInfo.FromStyle.Main.AbstractNum; |
| var multiLevelType = (string)abstractNum.Elements(W.multiLevelType).Attributes(W.val).FirstOrDefault(); |
| if (multiLevelType == "singleLevel") |
| this_ilvl = 0; |
| |
| SetParagraphLevel(paragraph, this_ilvl); |
| } |
| return; |
| } |
| |
| listItemInfo = new ListItemInfo(); |
| |
| int? style_ilvl = null; |
| bool? styleZeroNumId = null; |
| |
| if (paragraphStyleName != null) |
| { |
| listItemInfo.FromStyle = InitializeStyleListItemSource(numXDoc, stylesXDoc, paragraph, paragraphStyleName, |
| out style_ilvl, out styleZeroNumId); |
| } |
| |
| int? paragraph_ilvl = null; |
| bool? paragraphZeroNumId = null; |
| |
| if (paragraphNumberingProperties != null && paragraphNumberingProperties.Element(W.numId) != null) |
| { |
| listItemInfo.FromParagraph = InitializeParagraphListItemSource(numXDoc, stylesXDoc, paragraph, paragraphNumberingProperties, out paragraph_ilvl, out paragraphZeroNumId); |
| } |
| |
| if (styleZeroNumId == true && paragraphZeroNumId == null || |
| paragraphZeroNumId == true) |
| { |
| paragraph.AddAnnotation(NotAListItem); |
| AddListItemInfoIntoCache(numXDoc, paragraphStyleName, paragraphNumId, NotAListItem); |
| return; |
| } |
| |
| int ilvlToSet = 0; |
| if (paragraph_ilvl != null) |
| ilvlToSet = (int)paragraph_ilvl; |
| else if (style_ilvl != null) |
| ilvlToSet = (int)style_ilvl; |
| |
| if (listItemInfo.FromParagraph != null) |
| { |
| var abstractNum = listItemInfo.FromParagraph.Main.AbstractNum; |
| var multiLevelType = (string)abstractNum.Elements(W.multiLevelType).Attributes(W.val).FirstOrDefault(); |
| if (multiLevelType == "singleLevel") |
| ilvlToSet = 0; |
| } |
| else if (listItemInfo.FromStyle != null) |
| { |
| var abstractNum = listItemInfo.FromStyle.Main.AbstractNum; |
| var multiLevelType = (string)abstractNum.Elements(W.multiLevelType).Attributes(W.val).FirstOrDefault(); |
| if (multiLevelType == "singleLevel") |
| ilvlToSet = 0; |
| } |
| |
| SetParagraphLevel(paragraph, ilvlToSet); |
| |
| listItemInfo.IsListItem = listItemInfo.FromStyle != null || listItemInfo.FromParagraph != null; |
| paragraph.AddAnnotation(listItemInfo); |
| AddListItemInfoIntoCache(numXDoc, paragraphStyleName, paragraphNumId, listItemInfo); |
| } |
| |
| private static string GetParagraphStyleName(XDocument stylesXDoc, XElement paragraph) |
| { |
| var paragraphStyleName = (string)paragraph |
| .Elements(W.pPr) |
| .Elements(W.pStyle) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (paragraphStyleName == null) |
| paragraphStyleName = GetDefaultParagraphStyleName(stylesXDoc); |
| |
| return paragraphStyleName; |
| } |
| |
| private static bool FirstRunIsEmptySectionBreak(XElement paragraph) |
| { |
| var firstRun = paragraph |
| .DescendantsTrimmed(W.txbxContent) |
| .Where(d => d.Name == W.r) |
| .FirstOrDefault(); |
| |
| var hasTextElement = paragraph |
| .DescendantsTrimmed(W.txbxContent) |
| .Where(d => d.Name == W.r) |
| .Elements(W.t) |
| .Any(); |
| |
| if (firstRun == null || !hasTextElement) |
| { |
| if (paragraph |
| .Elements(W.pPr) |
| .Elements(W.sectPr) |
| .Any()) |
| return true; |
| } |
| return false; |
| } |
| |
| private static ListItemSource InitializeParagraphListItemSource(XDocument numXDoc, XDocument stylesXDoc, XElement paragraph, XElement paragraphNumberingProperties, out int? ilvl, out bool? zeroNumId) |
| { |
| zeroNumId = null; |
| |
| // Paragraph numbering properties must contain a numId. |
| int? numId = (int?)paragraphNumberingProperties |
| .Elements(W.numId) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| ilvl = (int?)paragraphNumberingProperties |
| .Elements(W.ilvl) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (numId == null) |
| { |
| zeroNumId = true; |
| return null; |
| } |
| |
| var num = numXDoc |
| .Root |
| .Elements(W.num) |
| .FirstOrDefault(n => (int)n.Attribute(W.numId) == numId); |
| if (num == null) |
| { |
| zeroNumId = true; |
| return null; |
| } |
| |
| zeroNumId = false; |
| |
| if (ilvl == null) |
| ilvl = 0; |
| |
| ListItemSource listItemSource = new ListItemSource(numXDoc, stylesXDoc, (int)numId); |
| |
| return listItemSource; |
| } |
| |
| private static ListItemSource InitializeStyleListItemSource(XDocument numXDoc, XDocument stylesXDoc, XElement paragraph, string paragraphStyleName, |
| out int? ilvl, out bool? zeroNumId) |
| { |
| zeroNumId = null; |
| XElement pPr = FormattingAssembler.ParagraphStyleRollup(paragraph, stylesXDoc, GetDefaultParagraphStyleName(stylesXDoc)); |
| if (pPr != null) |
| { |
| XElement styleNumberingProperties = pPr |
| .Elements(W.numPr) |
| .FirstOrDefault(); |
| |
| if (styleNumberingProperties != null && styleNumberingProperties.Element(W.numId) != null) |
| { |
| int numId = (int)styleNumberingProperties |
| .Elements(W.numId) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| ilvl = (int?)styleNumberingProperties |
| .Elements(W.ilvl) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (ilvl == null) |
| ilvl = 0; |
| |
| if (numId == 0) |
| { |
| zeroNumId = true; |
| return null; |
| } |
| |
| // make sure that the numId is valid |
| XElement num = numXDoc |
| .Root |
| .Elements(W.num) |
| .Where(e => (int)e.Attribute(W.numId) == numId) |
| .FirstOrDefault(); |
| |
| if (num == null) |
| { |
| zeroNumId = true; |
| return null; |
| } |
| |
| ListItemSource listItemSource = new ListItemSource(numXDoc, stylesXDoc, numId); |
| listItemSource.Style_ilvl = (int)ilvl; |
| |
| zeroNumId = false; |
| return listItemSource; |
| } |
| } |
| ilvl = null; |
| return null; |
| } |
| |
| private static string GetDefaultParagraphStyleName(XDocument stylesXDoc) |
| { |
| XElement defaultParagraphStyle; |
| string defaultParagraphStyleName = null; |
| |
| StylesInfo stylesInfo = stylesXDoc.Annotation<StylesInfo>(); |
| |
| if (stylesInfo != null) |
| defaultParagraphStyleName = stylesInfo.DefaultParagraphStyleName; |
| else |
| { |
| defaultParagraphStyle = stylesXDoc |
| .Root |
| .Elements(W.style) |
| .FirstOrDefault(s => |
| { |
| if ((string)s.Attribute(W.type) != "paragraph") |
| return false; |
| var defaultAttribute = s.Attribute(W._default); |
| var isDefault = false; |
| if (defaultAttribute != null && |
| (bool)s.Attribute(W._default).ToBoolean()) |
| isDefault = true; |
| return isDefault; |
| }); |
| defaultParagraphStyleName = null; |
| if (defaultParagraphStyle != null) |
| defaultParagraphStyleName = (string)defaultParagraphStyle.Attribute(W.styleId); |
| stylesInfo = new StylesInfo() |
| { |
| DefaultParagraphStyleName = defaultParagraphStyleName, |
| }; |
| stylesXDoc.AddAnnotation(stylesInfo); |
| } |
| return defaultParagraphStyleName; |
| } |
| |
| private static ListItemInfo GetListItemInfoFromCache(XDocument numXDoc, string styleName, int? numId) |
| { |
| string key = |
| (styleName == null ? "" : styleName) + |
| "|" + |
| (numId == null ? "" : numId.ToString()); |
| |
| var numXDocRoot = numXDoc.Root; |
| Dictionary<string, ListItemInfo> listItemInfoCache = |
| numXDocRoot.Annotation<Dictionary<string, ListItemInfo>>(); |
| if (listItemInfoCache == null) |
| { |
| listItemInfoCache = new Dictionary<string, ListItemInfo>(); |
| numXDocRoot.AddAnnotation(listItemInfoCache); |
| } |
| if (listItemInfoCache.ContainsKey(key)) |
| return listItemInfoCache[key]; |
| return null; |
| } |
| |
| private static void AddListItemInfoIntoCache(XDocument numXDoc, string styleName, int? numId, ListItemInfo listItemInfo) |
| { |
| string key = |
| (styleName == null ? "" : styleName) + |
| "|" + |
| (numId == null ? "" : numId.ToString()); |
| |
| var numXDocRoot = numXDoc.Root; |
| Dictionary<string, ListItemInfo> listItemInfoCache = |
| numXDocRoot.Annotation<Dictionary<string, ListItemInfo>>(); |
| if (listItemInfoCache == null) |
| { |
| listItemInfoCache = new Dictionary<string, ListItemInfo>(); |
| numXDocRoot.AddAnnotation(listItemInfoCache); |
| } |
| if (!listItemInfoCache.ContainsKey(key)) |
| listItemInfoCache.Add(key, listItemInfo); |
| } |
| |
| public class LevelNumbers |
| { |
| public int[] LevelNumbersArray; |
| } |
| |
| private class StylesInfo |
| { |
| public string DefaultParagraphStyleName; |
| } |
| |
| private class ParagraphInfo |
| { |
| public int Ilvl; |
| } |
| |
| private class ReverseAxis |
| { |
| public XElement PreviousParagraph; |
| } |
| |
| public static string RetrieveListItem(WordprocessingDocument wordDoc, XElement paragraph) |
| { |
| return RetrieveListItem(wordDoc, paragraph, null); |
| } |
| |
| public static string RetrieveListItem(WordprocessingDocument wordDoc, XElement paragraph, ListItemRetrieverSettings settings) |
| { |
| if (wordDoc.MainDocumentPart.NumberingDefinitionsPart == null) |
| return null; |
| |
| var listItemInfo = paragraph.Annotation<ListItemInfo>(); |
| if (listItemInfo == null) |
| InitializeListItemRetriever(wordDoc, settings); |
| |
| listItemInfo = paragraph.Annotation<ListItemInfo>(); |
| if (!listItemInfo.IsListItem) |
| return null; |
| |
| var numberingDefinitionsPart = wordDoc |
| .MainDocumentPart |
| .NumberingDefinitionsPart; |
| |
| if (numberingDefinitionsPart == null) |
| return null; |
| |
| StyleDefinitionsPart styleDefinitionsPart = wordDoc |
| .MainDocumentPart |
| .StyleDefinitionsPart; |
| |
| if (styleDefinitionsPart == null) |
| return null; |
| |
| var numXDoc = numberingDefinitionsPart.GetXDocument(); |
| var stylesXDoc = styleDefinitionsPart.GetXDocument(); |
| |
| var lvl = listItemInfo.Lvl(GetParagraphLevel(paragraph)); |
| |
| string lvlText = (string)lvl.Elements(W.lvlText).Attributes(W.val).FirstOrDefault(); |
| if (lvlText == null) |
| return null; |
| |
| var levelNumbersAnnotation = paragraph.Annotation<LevelNumbers>(); |
| if (levelNumbersAnnotation == null) |
| throw new OpenXmlPowerToolsException("Internal error"); |
| |
| int[] levelNumbers = levelNumbersAnnotation.LevelNumbersArray; |
| string languageIdentifier = GetLanguageIdentifier(paragraph, stylesXDoc); |
| string listItem = FormatListItem(listItemInfo, levelNumbers, GetParagraphLevel(paragraph), |
| lvlText, stylesXDoc, languageIdentifier, settings); |
| return listItem; |
| } |
| |
| private static string GetLanguageIdentifier(XElement paragraph, XDocument stylesXDoc) |
| { |
| var languageType = (string)paragraph |
| .DescendantsTrimmed(W.txbxContent) |
| .Where(d => d.Name == W.r) |
| .Attributes(PtOpenXml.LanguageType) |
| .FirstOrDefault(); |
| |
| string languageIdentifier = null; |
| |
| if (languageType == null || languageType == "western") |
| { |
| languageIdentifier = (string)paragraph |
| .Elements(W.r) |
| .Elements(W.rPr) |
| .Elements(W.lang) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| |
| if (languageIdentifier == null) |
| languageIdentifier = (string)stylesXDoc |
| .Root |
| .Elements(W.docDefaults) |
| .Elements(W.rPrDefault) |
| .Elements(W.rPr) |
| .Elements(W.lang) |
| .Attributes(W.val) |
| .FirstOrDefault(); |
| } |
| else if (languageType == "eastAsia") |
| { |
| languageIdentifier = (string)paragraph |
| .Elements(W.r) |
| .Elements(W.rPr) |
| .Elements(W.lang) |
| .Attributes(W.eastAsia) |
| .FirstOrDefault(); |
| |
| if (languageIdentifier == null) |
| languageIdentifier = (string)stylesXDoc |
| .Root |
| .Elements(W.docDefaults) |
| .Elements(W.rPrDefault) |
| .Elements(W.rPr) |
| .Elements(W.lang) |
| .Attributes(W.eastAsia) |
| .FirstOrDefault(); |
| } |
| else if (languageType == "bidi") |
| { |
| languageIdentifier = (string)paragraph |
| .Elements(W.r) |
| .Elements(W.rPr) |
| .Elements(W.lang) |
| .Attributes(W.bidi) |
| .FirstOrDefault(); |
| |
| if (languageIdentifier == null) |
| languageIdentifier = (string)stylesXDoc |
| .Root |
| .Elements(W.docDefaults) |
| .Elements(W.rPrDefault) |
| .Elements(W.rPr) |
| .Elements(W.lang) |
| .Attributes(W.bidi) |
| .FirstOrDefault(); |
| } |
| |
| |
| if (languageIdentifier == null) |
| languageIdentifier = "en-US"; |
| return languageIdentifier; |
| } |
| |
| private static void InitializeListItemRetriever(WordprocessingDocument wordDoc, ListItemRetrieverSettings settings) |
| { |
| foreach (var part in wordDoc.ContentParts()) |
| InitializeListItemRetrieverForPart(wordDoc, part, settings); |
| |
| #if false |
| foreach (var part in wordDoc.ContentParts()) |
| { |
| var xDoc = part.GetXDocument(); |
| var paras = xDoc |
| .Descendants(W.p) |
| .Where(p => |
| p.Annotation<ListItemInfo>() == null); |
| if (paras.Any()) |
| Console.WriteLine("Error"); |
| } |
| #endif |
| } |
| |
| private static void InitializeListItemRetrieverForPart(WordprocessingDocument wordDoc, OpenXmlPart part, ListItemRetrieverSettings settings) |
| { |
| var mainXDoc = part.GetXDocument(); |
| |
| var numPart = wordDoc.MainDocumentPart.NumberingDefinitionsPart; |
| if (numPart == null) |
| return; |
| var numXDoc = numPart.GetXDocument(); |
| |
| var stylesPart = wordDoc.MainDocumentPart.StyleDefinitionsPart; |
| if (stylesPart == null) |
| return; |
| var stylesXDoc = stylesPart.GetXDocument(); |
| |
| var rootNode = mainXDoc.Root; |
| |
| InitializeListItemRetrieverForStory(numXDoc, stylesXDoc, rootNode); |
| |
| var textBoxes = mainXDoc |
| .Root |
| .Descendants(W.txbxContent); |
| |
| foreach (var textBox in textBoxes) |
| InitializeListItemRetrieverForStory(numXDoc, stylesXDoc, textBox); |
| } |
| |
| private static void InitializeListItemRetrieverForStory(XDocument numXDoc, XDocument stylesXDoc, XElement rootNode) |
| { |
| var paragraphs = rootNode |
| .DescendantsTrimmed(W.txbxContent) |
| .Where(p => p.Name == W.p); |
| |
| foreach (var paragraph in paragraphs) |
| InitListItemInfo(numXDoc, stylesXDoc, paragraph); |
| |
| var abstractNumIds = paragraphs |
| .Select(paragraph => |
| { |
| ListItemInfo listItemInfo = paragraph.Annotation<ListItemInfo>(); |
| if (!listItemInfo.IsListItem) |
| return (int?)null; |
| return listItemInfo.AbstractNumId; |
| }) |
| .Where(a => a != null) |
| .Distinct() |
| .ToList(); |
| |
| // when debugging, it is sometimes useful to cause processing of a specific abstractNumId first. |
| // the following code enables this. |
| //int? abstractIdToProcessFirst = null; |
| //if (abstractIdToProcessFirst != null) |
| //{ |
| // abstractNumIds = (new[] { abstractIdToProcessFirst }) |
| // .Concat(abstractNumIds.Where(ani => ani != abstractIdToProcessFirst)) |
| // .ToList(); |
| //} |
| |
| foreach (var abstractNumId in abstractNumIds) |
| { |
| var listItems = paragraphs |
| .Where(paragraph => |
| { |
| var listItemInfo = paragraph.Annotation<ListItemInfo>(); |
| if (!listItemInfo.IsListItem) |
| return false; |
| return listItemInfo.AbstractNumId == abstractNumId; |
| }) |
| .ToList(); |
| |
| // annotate paragraphs with previous paragraphs so that we can look backwards with good perf |
| XElement prevParagraph = null; |
| foreach (var paragraph in listItems) |
| { |
| ReverseAxis reverse = new ReverseAxis() |
| { |
| PreviousParagraph = prevParagraph, |
| }; |
| paragraph.AddAnnotation(reverse); |
| prevParagraph = paragraph; |
| } |
| |
| var startOverrideAlreadyUsed = new List<int>(); |
| List<int> previous = null; |
| ListItemInfo[] listItemInfoInEffectForStartOverride = new ListItemInfo[] { |
| null, |
| null, |
| null, |
| null, |
| null, |
| null, |
| null, |
| null, |
| null, |
| null, |
| }; |
| foreach (var paragraph in listItems) |
| { |
| var listItemInfo = paragraph.Annotation<ListItemInfo>(); |
| var ilvl = GetParagraphLevel(paragraph); |
| listItemInfoInEffectForStartOverride[ilvl] = listItemInfo; |
| ListItemInfo listItemInfoInEffect = null; |
| if (ilvl > 0) |
| listItemInfoInEffect = listItemInfoInEffectForStartOverride[ilvl - 1]; |
| var levelNumbers = new List<int>(); |
| |
| for (int level = 0; level <= ilvl; level++) |
| { |
| var numId = listItemInfo.NumId; |
| var startOverride = listItemInfo.StartOverride(level); |
| int? inEffectStartOverride = null; |
| if (listItemInfoInEffect != null) |
| inEffectStartOverride = listItemInfoInEffect.StartOverride(level); |
| |
| if (level == ilvl) |
| { |
| var lvl = listItemInfo.Lvl(ilvl); |
| var lvlRestart = (int?)lvl.Elements(W.lvlRestart).Attributes(W.val).FirstOrDefault(); |
| if (lvlRestart != null) |
| { |
| var previousPara = PreviousParagraphsForLvlRestart(paragraph, (int)lvlRestart) |
| .FirstOrDefault(p => |
| { |
| var plvl = GetParagraphLevel(p); |
| return plvl == ilvl; |
| }); |
| if (previousPara != null) |
| previous = previousPara.Annotation<LevelNumbers>().LevelNumbersArray.ToList(); |
| } |
| } |
| |
| if (previous == null || |
| level >= previous.Count() || |
| (level == ilvl && startOverride != null && !startOverrideAlreadyUsed.Contains(numId))) |
| { |
| if (previous == null || level >= previous.Count()) |
| { |
| var start = listItemInfo.Start(level); |
| // only look at startOverride if the level that we're examining is same as the paragraph's level. |
| if (level == ilvl) |
| { |
| if (startOverride != null && !startOverrideAlreadyUsed.Contains(numId)) |
| { |
| startOverrideAlreadyUsed.Add(numId); |
| start = (int)startOverride; |
| } |
| else |
| { |
| if (startOverride != null) |
| start = (int)startOverride; |
| if (inEffectStartOverride != null && inEffectStartOverride > start) |
| start = (int)inEffectStartOverride; |
| } |
| } |
| levelNumbers.Add(start); |
| } |
| else |
| { |
| var start = listItemInfo.Start(level); |
| // only look at startOverride if the level that we're examining is same as the paragraph's level. |
| if (level == ilvl) |
| { |
| if (startOverride != null) |
| { |
| if (!startOverrideAlreadyUsed.Contains(numId)) |
| { |
| startOverrideAlreadyUsed.Add(numId); |
| start = (int)startOverride; |
| } |
| } |
| } |
| levelNumbers.Add(start); |
| } |
| } |
| else |
| { |
| int? thisNumber = null; |
| if (level == ilvl) |
| { |
| if (startOverride != null) |
| { |
| if (!startOverrideAlreadyUsed.Contains(numId)) |
| { |
| startOverrideAlreadyUsed.Add(numId); |
| thisNumber = (int)startOverride; |
| } |
| thisNumber = previous.ElementAt(level) + 1; |
| } |
| else |
| { |
| thisNumber = previous.ElementAt(level) + 1; |
| } |
| } |
| else |
| { |
| thisNumber = previous.ElementAt(level); |
| } |
| levelNumbers.Add((int)thisNumber); |
| } |
| } |
| var levelNumbersAnno = new LevelNumbers() |
| { |
| LevelNumbersArray = levelNumbers.ToArray() |
| }; |
| paragraph.AddAnnotation(levelNumbersAnno); |
| previous = levelNumbers; |
| } |
| } |
| } |
| |
| private static IEnumerable<XElement> PreviousParagraphsForLvlRestart(XElement paragraph, int ilvl) |
| { |
| var current = paragraph; |
| while (true) |
| { |
| var ra = current.Annotation<ReverseAxis>(); |
| if (ra == null || ra.PreviousParagraph == null) |
| yield break; |
| var raLvl = GetParagraphLevel(ra.PreviousParagraph); |
| if (raLvl < ilvl) |
| yield break; |
| yield return ra.PreviousParagraph; |
| current = ra.PreviousParagraph; |
| } |
| } |
| |
| private static string FormatListItem(ListItemInfo lii, int[] levelNumbers, int ilvl, |
| string lvlText, XDocument styles, string languageCultureName, ListItemRetrieverSettings settings) |
| { |
| string[] formatTokens = GetFormatTokens(lvlText).ToArray(); |
| XElement lvl = lii.Lvl(ilvl); |
| bool isLgl = lvl.Elements(W.isLgl).Any(); |
| string listItem = formatTokens.Select((t, l) => |
| { |
| if (t.Substring(0, 1) != "%") |
| return t; |
| int indentationLevel; |
| if (!Int32.TryParse(t.Substring(1), out indentationLevel)) |
| return t; |
| indentationLevel -= 1; |
| if (indentationLevel >= levelNumbers.Length) |
| indentationLevel = levelNumbers.Length - 1; |
| int levelNumber = levelNumbers[indentationLevel]; |
| string levelText = null; |
| XElement rlvl = lii.Lvl(indentationLevel); |
| string numFmtForLevel = (string)rlvl.Elements(W.numFmt).Attributes(W.val).FirstOrDefault(); |
| if (numFmtForLevel == null) |
| { |
| var numFmtElement = rlvl.Elements(MC.AlternateContent).Elements(MC.Choice).Elements(W.numFmt).FirstOrDefault(); |
| if (numFmtElement != null && (string)numFmtElement.Attribute(W.val) == "custom") |
| numFmtForLevel = (string)numFmtElement.Attribute(W.format); |
| } |
| if (numFmtForLevel != "none") |
| { |
| if (isLgl && numFmtForLevel != "decimalZero") |
| numFmtForLevel = "decimal"; |
| } |
| if (languageCultureName != null && settings != null) |
| { |
| if (settings.ListItemTextImplementations.ContainsKey(languageCultureName)) |
| { |
| var impl = settings.ListItemTextImplementations[languageCultureName]; |
| levelText = impl(languageCultureName, levelNumber, numFmtForLevel); |
| } |
| } |
| if (levelText == null) |
| levelText = ListItemTextGetter_Default.GetListItemText(languageCultureName, levelNumber, numFmtForLevel); |
| return levelText; |
| }).StringConcatenate(); |
| return listItem; |
| } |
| |
| private static IEnumerable<string> GetFormatTokens(string lvlText) |
| { |
| int i = 0; |
| while (true) |
| { |
| if (i >= lvlText.Length) |
| yield break; |
| if (lvlText[i] == '%' && i <= lvlText.Length - 2) |
| { |
| yield return lvlText.Substring(i, 2); |
| i += 2; |
| continue; |
| } |
| int percentIndex = lvlText.IndexOf('%', i); |
| if (percentIndex == -1 || percentIndex > lvlText.Length - 2) |
| { |
| yield return lvlText.Substring(i); |
| yield break; |
| } |
| yield return lvlText.Substring(i, percentIndex - i); |
| yield return lvlText.Substring(percentIndex, 2); |
| i = percentIndex + 2; |
| } |
| } |
| |
| } |
| } |