/*************************************************************************** Copyright (c) Microsoft Corporation 2010. 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 ***************************************************************************/ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.Linq; using DocumentFormat.OpenXml.Packaging; namespace OpenXmlPowerTools { public partial class WmlDocument : OpenXmlPowerToolsDocument { public string RetrieveListItem(XElement paragraph, string bulletReplacementString) { return ListItemRetriever.RetrieveListItem(this, paragraph, bulletReplacementString); } } public class ListItemRetriever { private class ListItemInfo { public bool IsListItem; public XElement Lvl; public int? Start; public int? AbstractNumId; public ListItemInfo(bool isListItem) { IsListItem = isListItem; } } private class LevelNumbers { public int[] LevelNumbersArray; } private static ListItemInfo GetListItemInfoByNumIdAndIlvl(XDocument numbering, XDocument styles, int numId, int ilvl) { if (numId == 0) return new ListItemInfo(false); ListItemInfo listItemInfo = new ListItemInfo(true); XElement num = numbering.Root.Elements(W.num) .Where(e => (int)e.Attribute(W.numId) == numId).FirstOrDefault(); if (num == null) return new ListItemInfo(false); listItemInfo.AbstractNumId = (int?)num.Elements(W.abstractNumId) .Attributes(W.val).FirstOrDefault(); XElement lvlOverride = num.Elements(W.lvlOverride) .Where(e => (int)e.Attribute(W.ilvl) == ilvl).FirstOrDefault(); // If there is a w:lvlOverride element, and if the w:lvlOverride contains a // w:lvl element, then return it. Otherwise, go look in the abstract numbering // definition. if (lvlOverride != null) { // Get the startOverride, if there is one. listItemInfo.Start = (int?)num.Elements(W.lvlOverride) .Where(o => (int)o.Attribute(W.ilvl) == ilvl).Elements(W.startOverride) .Attributes(W.val).FirstOrDefault(); listItemInfo.Lvl = lvlOverride.Element(W.lvl); if (listItemInfo.Lvl != null) { if (listItemInfo.Start == null) listItemInfo.Start = (int?)listItemInfo.Lvl.Elements(W.start) .Attributes(W.val).FirstOrDefault(); return listItemInfo; } } int? a = listItemInfo.AbstractNumId; XElement abstractNum = numbering.Root.Elements(W.abstractNum) .Where(e => (int)e.Attribute(W.abstractNumId) == a).FirstOrDefault(); string numStyleLink = (string)abstractNum.Elements(W.numStyleLink) .Attributes(W.val).FirstOrDefault(); if (numStyleLink != null) { XElement style = styles.Root.Elements(W.style) .Where(e => (string)e.Attribute(W.styleId) == numStyleLink) .FirstOrDefault(); XElement numPr = style.Elements(W.pPr).Elements(W.numPr).FirstOrDefault(); int lNumId = (int)numPr.Elements(W.numId).Attributes(W.val) .FirstOrDefault(); return GetListItemInfoByNumIdAndIlvl(numbering, styles, lNumId, ilvl); } for (int l = ilvl; l >= 0; --l) { listItemInfo.Lvl = abstractNum.Elements(W.lvl) .Where(e => (int)e.Attribute(W.ilvl) == l).FirstOrDefault(); if (listItemInfo.Lvl == null) continue; if (listItemInfo.Start == null) listItemInfo.Start = (int?)listItemInfo.Lvl.Elements(W.start) .Attributes(W.val).FirstOrDefault(); return listItemInfo; } return new ListItemInfo(false); } private static ListItemInfo GetListItemInfoByNumIdAndStyleId(XDocument numbering, XDocument styles, int numId, string paragraphStyle) { // If you have to find the w:lvl by style id, then we can't find it in the // w:lvlOverride, as that requires that you have determined the level already. ListItemInfo listItemInfo = new ListItemInfo(true); XElement num = numbering.Root.Elements(W.num) .Where(e => (int)e.Attribute(W.numId) == numId).FirstOrDefault(); listItemInfo.AbstractNumId = (int)num.Elements(W.abstractNumId) .Attributes(W.val).FirstOrDefault(); int? a = listItemInfo.AbstractNumId; XElement abstractNum = numbering.Root.Elements(W.abstractNum) .Where(e => (int)e.Attribute(W.abstractNumId) == a).FirstOrDefault(); string numStyleLink = (string)abstractNum.Element(W.numStyleLink) .Attributes(W.val).FirstOrDefault(); if (numStyleLink != null) { XElement style = styles.Root.Elements(W.style) .Where(e => (string)e.Attribute(W.styleId) == numStyleLink) .FirstOrDefault(); XElement numPr = style.Elements(W.pPr).Elements(W.numPr).FirstOrDefault(); int lNumId = (int)numPr.Elements(W.numId).Attributes(W.val).FirstOrDefault(); return GetListItemInfoByNumIdAndStyleId(numbering, styles, lNumId, paragraphStyle); } listItemInfo.Lvl = abstractNum.Elements(W.lvl) .Where(e => (string)e.Element(W.pStyle) == paragraphStyle).FirstOrDefault(); listItemInfo.Start = (int?)listItemInfo.Lvl.Elements(W.start).Attributes(W.val) .FirstOrDefault(); return listItemInfo; } private static ListItemInfo GetListItemInfo(XDocument numbering, XDocument styles, XElement paragraph) { // The following is an optimization - only determine ListItemInfo once for a // paragraph. ListItemInfo listItemInfo = paragraph.Annotation(); if (listItemInfo != null) return listItemInfo; XElement paragraphNumberingProperties = paragraph.Elements(W.pPr) .Elements(W.numPr).FirstOrDefault(); string paragraphStyle = (string)paragraph.Elements(W.pPr).Elements(W.pStyle) .Attributes(W.val).FirstOrDefault(); if (paragraphNumberingProperties != null && paragraphNumberingProperties.Element(W.numId) != null) { // Paragraph numbering properties must contain a numId. int numId = (int)paragraphNumberingProperties.Elements(W.numId) .Attributes(W.val).FirstOrDefault(); int? ilvl = (int?)paragraphNumberingProperties.Elements(W.ilvl) .Attributes(W.val).FirstOrDefault(); if (ilvl != null) { listItemInfo = GetListItemInfoByNumIdAndIlvl(numbering, styles, numId, (int)ilvl); paragraph.AddAnnotation(listItemInfo); return listItemInfo; } if (paragraphStyle != null) { listItemInfo = GetListItemInfoByNumIdAndStyleId(numbering, styles, numId, paragraphStyle); paragraph.AddAnnotation(listItemInfo); return listItemInfo; } listItemInfo = new ListItemInfo(false); paragraph.AddAnnotation(listItemInfo); return listItemInfo; } if (paragraphStyle != null) { XElement style = styles.Root.Elements(W.style).Where(s => (string)s.Attribute(W.type) == "paragraph" && (string)s.Attribute(W.styleId) == paragraphStyle).FirstOrDefault(); if (style != null) { XElement styleNumberingProperties = style.Elements(W.pPr) .Elements(W.numPr).FirstOrDefault(); if (styleNumberingProperties != null && styleNumberingProperties.Element(W.numId) != null) { int numId = (int)styleNumberingProperties.Elements(W.numId) .Attributes(W.val).FirstOrDefault(); int? ilvl = (int?)styleNumberingProperties.Elements(W.ilvl) .Attributes(W.val).FirstOrDefault(); if (ilvl == null) ilvl = 0; listItemInfo = GetListItemInfoByNumIdAndIlvl(numbering, styles, numId, (int)ilvl); paragraph.AddAnnotation(listItemInfo); return listItemInfo; } } } listItemInfo = new ListItemInfo(false); paragraph.AddAnnotation(listItemInfo); return listItemInfo; } private static IEnumerable ParagraphsToConsiderWhenCounting( XDocument numbering, XDocument styles, XElement paragraph, int levelNumber) { ListItemInfo listItemInfo = GetListItemInfo(numbering, styles, paragraph); int? lvlRestart = (int?)listItemInfo.Lvl.Elements(W.lvlRestart) .Attributes(W.val).FirstOrDefault(); int paragraphLevel = (int)listItemInfo.Lvl.Attribute(W.ilvl); IEnumerable paragraphsBeforeSelf = paragraph .ElementsBeforeSelfReverseDocumentOrder().Where(e => e.Name == W.p); foreach (var p in paragraphsBeforeSelf) { ListItemInfo pListItemInfo = GetListItemInfo(numbering, styles, p); if (!pListItemInfo.IsListItem || pListItemInfo.AbstractNumId != listItemInfo.AbstractNumId) continue; LevelNumbers pLevelNumbers = p.Annotation(); int pLevel = (int)pListItemInfo.Lvl.Attribute(W.ilvl); if (pLevel > levelNumber) yield return pLevelNumbers; if (lvlRestart == null) { if (pLevel < levelNumber) yield break; } else { if (pLevel < levelNumber && pLevel > lvlRestart - 1) continue; else if (pLevel < levelNumber) yield break; } yield return pLevelNumbers; } } private static int GetLevelNumberForLevel(XDocument numbering, XDocument styles, XElement paragraph, int level) { ListItemInfo listItemInfo = GetListItemInfo(numbering, styles, paragraph); int paragraphLevel = (int)listItemInfo.Lvl.Attribute(W.ilvl); var paragraphsToConsider = ParagraphsToConsiderWhenCounting(numbering, styles, paragraph, level) .Select(o => o.LevelNumbersArray.Take(paragraphLevel + 1) .Select(z => z.ToString() + ".").StringConcatenate()) .GroupBy(o => o); int levelNumberForLevel = paragraphsToConsider.Count(); return levelNumberForLevel; } private static int[] GetLevelNumbers(XDocument numbering, XDocument styles, XElement paragraph) { IEnumerable paragraphsBeforeSelf = paragraph .ElementsBeforeSelfReverseDocumentOrder().Where(e => e.Name == W.p); int level; ListItemInfo listItemInfo = GetListItemInfo(numbering, styles, paragraph); level = (int)listItemInfo.Lvl.Attribute(W.ilvl); List levelNumbers = new List(); for (int indentationLevel = 0; indentationLevel <= level; ++indentationLevel) { XElement currentIndentLvl = GetRelatedLevel(listItemInfo.Lvl, indentationLevel); int? start = (int?)currentIndentLvl.Elements(W.start).Attributes(W.val) .FirstOrDefault(); if (start == null) start = 1; XElement paragraphWithSameAbstractNumId = paragraphsBeforeSelf .FirstOrDefault(p => { ListItemInfo pListItemInfo = GetListItemInfo(numbering, styles, p); return pListItemInfo.IsListItem && pListItemInfo.AbstractNumId == listItemInfo.AbstractNumId; }); if (paragraphWithSameAbstractNumId != null) { LevelNumbers pLevelNumbers = paragraphWithSameAbstractNumId .Annotation(); if (pLevelNumbers.LevelNumbersArray.Length > indentationLevel) { if (indentationLevel == level) levelNumbers.Add( pLevelNumbers.LevelNumbersArray[indentationLevel] + 1); else levelNumbers.Add(pLevelNumbers .LevelNumbersArray[indentationLevel]); continue; } } if (level == indentationLevel) { int c1 = GetLevelNumberForLevel(numbering, styles, paragraph, indentationLevel); int? start2 = listItemInfo.Start; if (start2 == null) start2 = 1; levelNumbers.Add(c1 + (int)start2); continue; } levelNumbers.Add((int)start); } return levelNumbers.ToArray(); } private static IEnumerable 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; } } private static string[] RomanOnes = { "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" }; private static string[] RomanTens = { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" }; private static string[] RomanHundreds = { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM", "M" }; private static string[] RomanThousands = { "", "M", "MM", "MMM", "MMMM", "MMMMM", "MMMMMM", "MMMMMMM", "MMMMMMMM", "MMMMMMMMM", "MMMMMMMMMM" }; private static string[] OneThroughNineteen = { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" }; private static string[] Tens = { "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety" }; private static string[] OrdinalOneThroughNineteen = { "first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth", "tenth", "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth", "sixteenth", "seventeenth", "eighteenth", "nineteenth" }; private static string[] OrdinalTenths = { "tenth", "twentieth", "thirtieth", "fortieth", "fiftieth", "sixtieth", "seventieth", "eightieth", "ninetieth" }; private static string GetLevelText(int levelNumber, string numFmt) { if (numFmt == "decimal") { return levelNumber.ToString(); } if (numFmt == "decimalZero") { if (levelNumber <= 9) return "0" + levelNumber.ToString(); else return levelNumber.ToString(); } if (numFmt == "upperRoman") { int ones = levelNumber % 10; int tens = (levelNumber % 100) / 10; int hundreds = (levelNumber % 1000) / 100; int thousands = levelNumber / 1000; return RomanThousands[thousands] + RomanHundreds[hundreds] + RomanTens[tens] + RomanOnes[ones]; } if (numFmt == "lowerRoman") { int ones = levelNumber % 10; int tens = (levelNumber % 100) / 10; int hundreds = (levelNumber % 1000) / 100; int thousands = levelNumber / 1000; return (RomanThousands[thousands] + RomanHundreds[hundreds] + RomanTens[tens] + RomanOnes[ones]).ToLower(); } if (numFmt == "upperLetter") { string a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; int c = (levelNumber - 1) / 26; int n = (levelNumber - 1) % 26; char x = a[n]; return "".PadRight(c + 1, x); } if (numFmt == "lowerLetter") { string a = "abcdefghijklmnopqrstuvwxyz"; int c = (levelNumber - 1) / 26; int n = (levelNumber - 1) % 26; char x = a[n]; return "".PadRight(c + 1, x); } if (numFmt == "ordinal") { string suffix; if (levelNumber % 100 == 11 || levelNumber % 100 == 12 || levelNumber % 100 == 13) suffix = "th"; else if (levelNumber % 10 == 1) suffix = "st"; else if (levelNumber % 10 == 2) suffix = "nd"; else if (levelNumber % 10 == 3) suffix = "rd"; else suffix = "th"; return levelNumber.ToString() + suffix; } if (numFmt == "cardinalText") { string result = ""; int t1 = levelNumber / 1000; int t2 = levelNumber % 1000; if (t1 >= 1) result += OneThroughNineteen[t1 - 1] + " thousand"; if (t1 >= 1 && t2 == 0) return result.Substring(0, 1).ToUpper() + result.Substring(1); if (t1 >= 1) result += " "; int h1 = (levelNumber % 1000) / 100; int h2 = levelNumber % 100; if (h1 >= 1) result += OneThroughNineteen[h1 - 1] + " hundred"; if (h1 >= 1 && h2 == 0) return result.Substring(0, 1).ToUpper() + result.Substring(1); if (h1 >= 1) result += " "; int z = levelNumber % 100; if (z <= 19) result += OneThroughNineteen[z - 1]; else { int x = z / 10; int r = z % 10; result += Tens[x - 1]; if (r >= 1) result += "-" + OneThroughNineteen[r - 1]; } return result.Substring(0, 1).ToUpper() + result.Substring(1); } if (numFmt == "ordinalText") { string result = ""; int t1 = levelNumber / 1000; int t2 = levelNumber % 1000; if (t1 >= 1 && t2 != 0) result += OneThroughNineteen[t1 - 1] + " thousand"; if (t1 >= 1 && t2 == 0) { result += OneThroughNineteen[t1 - 1] + " thousandth"; return result.Substring(0, 1).ToUpper() + result.Substring(1); } if (t1 >= 1) result += " "; int h1 = (levelNumber % 1000) / 100; int h2 = levelNumber % 100; if (h1 >= 1 && h2 != 0) result += OneThroughNineteen[h1 - 1] + " hundred"; if (h1 >= 1 && h2 == 0) { result += OneThroughNineteen[h1 - 1] + " hundredth"; return result.Substring(0, 1).ToUpper() + result.Substring(1); } if (h1 >= 1) result += " "; int z = levelNumber % 100; if (z <= 19) result += OrdinalOneThroughNineteen[z - 1]; else { int x = z / 10; int r = z % 10; if (r == 0) result += OrdinalTenths[x - 1]; else result += Tens[x - 1]; if (r >= 1) result += "-" + OrdinalOneThroughNineteen[r - 1]; } return result.Substring(0, 1).ToUpper() + result.Substring(1); } if (numFmt == "bullet") return ""; // This method needs to be enhanced to support all languages and // all number formats. return levelNumber.ToString(); } private static XElement GetRelatedLevel(XElement lvl, int level) { XElement parent = lvl.Parent; XElement newLvl; if (parent.Name == W.lvlOverride) { newLvl = parent.Parent.Elements(W.lvlOverride).Elements(W.lvl) .Where(e => (int)e.Attribute(W.ilvl) == level).FirstOrDefault(); if (newLvl != null) return newLvl; int abstractNumId = (int)parent.Parent.Elements(W.abstractNumId) .Attributes(W.val).FirstOrDefault(); XElement abstractNum = lvl.Ancestors().Last().Elements(W.abstractNum) .Where(e => (int)e.Attribute(W.abstractNumId) == abstractNumId) .FirstOrDefault(); newLvl = abstractNum.Elements(W.lvl) .Where(e => (int)e.Attribute(W.ilvl) == level).FirstOrDefault(); return newLvl; } newLvl = parent.Elements(W.lvl).Where(e => (int)e.Attribute(W.ilvl) == level) .FirstOrDefault(); return newLvl; } private static string FormatListItem(XElement lvl, int[] levelNumbers, string lvlText, string bulletReplacementString) { if (bulletReplacementString != null) { StringBuilder sb = new StringBuilder(lvlText); sb.Replace("\xF076", bulletReplacementString); sb.Replace("\xF0D8", bulletReplacementString); sb.Replace("\xF0A7", bulletReplacementString); sb.Replace("\xF0B7", bulletReplacementString); sb.Replace("\xF0A8", bulletReplacementString); sb.Replace("\xF0FC", bulletReplacementString); lvlText = sb.ToString(); } string[] formatTokens = GetFormatTokens(lvlText).ToArray(); bool isLgl = lvl.Elements(W.isLgl).Any(); string listItem = formatTokens.Select((t, l) => { if (t.Substring(0, 1) != "%") return t; int indentationLevel = Int32.Parse(t.Substring(1)) - 1; int levelNumber = levelNumbers[indentationLevel]; string levelText; XElement rlvl = GetRelatedLevel(lvl, indentationLevel); string numFmtForLevel = (string)rlvl.Elements(W.numFmt).Attributes(W.val) .FirstOrDefault(); if (isLgl && numFmtForLevel != "decimalZero") numFmtForLevel = "decimal"; levelText = GetLevelText(levelNumber, numFmtForLevel); return levelText; }).StringConcatenate(); return listItem + " "; } public static string RetrieveListItem(WmlDocument document, XElement paragraph, string bulletReplacementString) { using (OpenXmlMemoryStreamDocument streamDoc = new OpenXmlMemoryStreamDocument(document)) { using (WordprocessingDocument wdoc = streamDoc.GetWordprocessingDocument()) { return RetrieveListItem(wdoc, paragraph, bulletReplacementString); } } } public static string RetrieveListItem(WordprocessingDocument wordDoc, XElement paragraph, string bulletReplacementString) { string pt = paragraph.Elements(W.r).Elements(W.t).Select(e => e.Value) .StringConcatenate(); NumberingDefinitionsPart numberingDefinitionsPart = wordDoc.MainDocumentPart.NumberingDefinitionsPart; if (numberingDefinitionsPart == null) return null; StyleDefinitionsPart styleDefinitionsPart = wordDoc.MainDocumentPart .StyleDefinitionsPart; if (styleDefinitionsPart == null) return null; XDocument numbering = numberingDefinitionsPart.GetXDocument(); XDocument styles = styleDefinitionsPart.GetXDocument(); ListItemInfo listItemInfo = GetListItemInfo(numbering, styles, paragraph); if (listItemInfo.IsListItem) { string lvlText = (string)listItemInfo.Lvl.Elements(W.lvlText) .Attributes(W.val).FirstOrDefault(); int[] levelNumbers = GetLevelNumbers(numbering, styles, paragraph); paragraph.AddAnnotation(new LevelNumbers() { LevelNumbersArray = levelNumbers }); string listItem = FormatListItem(listItemInfo.Lvl, levelNumbers, lvlText, bulletReplacementString); return listItem; } return null; } } }