Remove Bookmarks in Word Document using C#

What to do if you need to remove bookmark and its entire content from the document altogether. The only method existing in API for that is setting Bookmark.Text to an empty string. But that will work only for the simplest of bookmarks. For bookmark spanning multiple nodes with mixed content, like paragraph, tables, it will fail with a dreaded “Start and end node should have the same grand parent” exception. It will also fail when bookmark spans several sections and bookmark content is crossing the section border.

Taking into account this and the fact that lots of users are requesting the robust bookmark removal capability we have decided to provide a sample code to achieve this functionality. We have already tested this code on several ‘problem’ documents and it works fine as far as I can see. However, it is up to you to try it for yourself and find out if it works for your documents. Please report all encountered problems in the forum. We will try to fix them as soon as they will be reported. Then, after some time and community testing we are going to include this method into Aspose.Words API.

C# Code to Remove Bookmarks in Word Document

  1. private void RemoveBookmarkWithContent(Bookmark bookmark)
  2. {
  3. // We need to store other bookmark nodes here, to move them away from the removed area.
  4. Hashtable bookmarkNames = new Hashtable();
  5. Hashtable bookmarkStarts = new Hashtable();
  6. Hashtable bookmarkEnds = new Hashtable();
  7. ArrayList nodesToRemove = new ArrayList();
  8. BookmarkStart bookmarkStart = bookmark.BookmarkStart;
  9. BookmarkEnd bookmarkEnd = bookmark.BookmarkEnd;
  10. Document doc = bookmarkStart.Document;
  11. Paragraph lastParagraph = doc.LastSection.Body.LastParagraph;
  12. Node node = SeekFirstNodeOfBookmarkRange(bookmarkStart);
  13. Paragraph endPara = FindParagraphNextAfterBookmark(bookmarkEnd);
  14. // Iterate over all nodes that contain or are between bookmark start and end nodes.
  15. while(node != bookmarkEnd)
  16. {
  17. node = node.NextPreOrder(doc);
  18. // BookmarkStart/BookmarkEnd are saved to be handled separately later.
  19. // All other nodes are collected as candidates for removal.
  20. if (!StoreIfBookmark(bookmarkNames, bookmarkStarts, bookmarkEnds, node))
  21. nodesToRemove.Add(node);
  22. }
  23. foreach (string name in bookmarkNames.Keys)
  24. {
  25. if ((bookmarkStarts.ContainsKey(name)) && (bookmarkEnds.ContainsKey(name)))
  26. {
  27. // If bookmark is nested, remove it altogether.
  28. RemoveBookmarkNode(name, bookmarkStarts);
  29. RemoveBookmarkNode(name, bookmarkEnds);
  30. }
  31. else
  32. {
  33. // If bookmark is overlapping, move the contained start/end node to the next paragraph after removed range.
  34. if (bookmarkStarts.ContainsKey(name))
  35. MoveBookmarkNode(name, bookmarkStarts, endPara);
  36. else
  37. MoveBookmarkNode(name, bookmarkEnds, endPara);
  38. }
  39. }
  40. bool hasNodesToRemove = true;
  41. while(hasNodesToRemove)
  42. {
  43. hasNodesToRemove = false;
  44. for (int i = 0; i < nodesToRemove.Count; i++)
  45. {
  46. Node nodeToRemove = (Node)nodesToRemove[i];
  47. // Skip already removed nodes.
  48. if (nodeToRemove.ParentNode == null)
  49. continue;
  50. // Skip nodes that have child nodes.
  51. if (nodeToRemove.IsComposite && (nodeToRemove as CompositeNode).HasChildNodes)
  52. continue;
  53. // Do not remove node if it is the last paragraph in the document.
  54. if (nodeToRemove == lastParagraph)
  55. continue;
  56. // Remove node.
  57. nodeToRemove.Remove();
  58. // If at least one node was removed in loop, then the loop will be repeated.
  59. hasNodesToRemove = true;
  60. }
  61. }
  62. }
  63. private Node SeekFirstNodeOfBookmarkRange(BookmarkStart bookmarkStart)
  64. {
  65. Node node = bookmarkStart;
  66. Document doc = node.Document;
  67. // Bookmark nodes located immediately before start of our bookmark should also be included in the removal process.
  68. do
  69. {
  70. node = node.PreviousPreOrder(doc);
  71. }
  72. while(IsBookmarkNode(node));
  73. // Look back from the bookmark start node to include containing nodes into removal process.
  74. while(node.IsComposite)
  75. {
  76. Node prevNode = node.PreviousPreOrder(doc);
  77. if (prevNode == null)
  78. break;
  79. else
  80. node = prevNode;
  81. }
  82. return node;
  83. }
  84. private Paragraph FindParagraphNextAfterBookmark(BookmarkEnd bookmarkEnd)
  85. {
  86. // Find the paragraph that is next to removed bookmark range.
  87. // It will be used to move all bookmark start/end nodes belonging to bookmarks overlapping our bookmark,
  88. // so that they will be preserved after this bookmark removal.
  89. Paragraph para;
  90. Node node = bookmarkEnd;
  91. Document doc = node.Document;
  92. // It can be that the paragraph containing bookmark end node if the last paragraph in the bookmark range
  93. // contains other nodes beside BookmarkEnd or is the last unremovable paragraph in the document.
  94. if (node.NextSibling != null || node.ParentNode == doc.LastSection.Body.LastParagraph)
  95. {
  96. para = (Paragraph)bookmarkEnd.ParentNode;
  97. }
  98. // Or it can be the paragraph next to it.
  99. else
  100. {
  101. while(node.NodeType != NodeType.Paragraph)
  102. {
  103. node = node.NextPreOrder(doc);
  104. }
  105. para = (Paragraph)node;
  106. }
  107. return para;
  108. }
  109. private bool IsBookmarkNode(Node node)
  110. {
  111. return (node.NodeType == NodeType.BookmarkStart) || (node.NodeType == NodeType.BookmarkEnd);
  112. }
  113. private bool StoreIfBookmark(Hashtable bookmarkNames, Hashtable bookmarkStarts, Hashtable bookmarkEnds, Node node)
  114. {
  115. if (node.NodeType == NodeType.BookmarkStart)
  116. {
  117. BookmarkStart bookmarkStart = (BookmarkStart)node;
  118. bookmarkNames[bookmarkStart.Name] = null;
  119. bookmarkStarts.Add(bookmarkStart.Name, bookmarkStart);
  120. return true;
  121. }
  122. else if (node.NodeType == NodeType.BookmarkEnd)
  123. {
  124. BookmarkEnd bookmarkEnd = (BookmarkEnd)node;
  125. bookmarkNames[bookmarkEnd.Name] = null;
  126. bookmarkEnds.Add(bookmarkEnd.Name, bookmarkEnd);
  127. return true;
  128. }
  129. return false;
  130. }
  131. private Node RemoveBookmarkNode(string name, Hashtable bookmarkNodes)
  132. {
  133. Node node = (Node)bookmarkNodes[name];
  134. node.Remove();
  135. bookmarkNodes.Remove(name);
  136. return node;
  137. }
  138. private void MoveBookmarkNode(string name, Hashtable bookmarkNodes, Paragraph para)
  139. {
  140. para.PrependChild(RemoveBookmarkNode(name, bookmarkNodes));
  141. }
  142. [VB .NET]
  143. Private Sub RemoveBookmarkWithContent(ByVal bookmark As Bookmark)
  144. ‘ We need to store other bookmark nodes here, to move them away from the removed area.
  145. Dim bookmarkNames As Hashtable = New Hashtable
  146. Dim bookmarkStarts As Hashtable = New Hashtable
  147. Dim bookmarkEnds As Hashtable = New Hashtable
  148. Dim nodesToRemove As ArrayList = New ArrayList
  149. Dim bookmarkStart As BookmarkStart = bookmark.BookmarkStart
  150. Dim bookmarkEnd As BookmarkEnd = bookmark.BookmarkEnd
  151. Dim node As Node = bookmarkStart
  152. Dim doc As Document = node.Document
  153. Dim lastParagraph As Paragraph = doc.LastSection.Body.LastParagraph
  154. ‘ Bookmark nodes located immediately before start of our bookmark should also be included in the removal process.
  155. Do
  156. node = node.PreviousPreOrder(doc)
  157. Loop While IsBookmarkNode(node)
  158. ‘ Look back from the bookmark start node to include containing nodes into removal process.
  159. Do While node.IsComposite
  160. Dim prevNode As Node = node.PreviousPreOrder(doc)
  161. If prevNode Is Nothing Then
  162. Exit Do
  163. Else
  164. node = prevNode
  165. End If
  166. Loop
  167. ‘ Find the paragraph that is next to removed bookmark range.
  168. ‘ It will be used to move all bookmark start/end nodes belonging to bookmarks overlapping our bookmark,
  169. ‘ so that they will be preserved after this bookmark removal.
  170. Dim endPara As Paragraph
  171. ‘ It can be the paragraph containing bookmark end node if the last paragraph in the bookmark range
  172. ‘ contains other nodes beside BookmarkEnd or is the last unremovable paragraph in the document.
  173. If Not bookmarkEnd.NextSibling Is Nothing OrElse bookmarkEnd.ParentNode Is lastParagraph Then
  174. endPara = CType(bookmarkEnd.ParentNode, Paragraph)
  175. ‘ Or it can be the paragraph next to it.
  176. Else
  177. Do While node.NodeType <> NodeType.Paragraph
  178. node = node.NextPreOrder(doc)
  179. Loop
  180. endPara = CType(node, Paragraph)
  181. End If
  182. ‘ Iterate over all nodes that contain or are between bookmark start and end nodes.
  183. Do While Not node Is bookmarkEnd
  184. node = node.NextPreOrder(doc)
  185. ‘ BookmarkStart/BookmarkEnd are saved to be handled separately later.
  186. ‘ All other nodes are collected as candidates for removal.
  187. If (Not StoreIfBookmark(bookmarkNames, bookmarkStarts, bookmarkEnds, node)) Then
  188. nodesToRemove.Add(node)
  189. End If
  190. Loop
  191. For Each name As String In bookmarkNames.Keys
  192. If (bookmarkStarts.ContainsKey(name)) AndAlso (bookmarkEnds.ContainsKey(name)) Then
  193. ‘ If bookmark is nested, remove it altogether.
  194. RemoveBookmarkNode(name, bookmarkStarts)
  195. RemoveBookmarkNode(name, bookmarkEnds)
  196. Else
  197. ‘ If bookmark is overlapping, move the contained start/end node to the next paragraph after removed range.
  198. If bookmarkStarts.ContainsKey(name) Then
  199. MoveBookmarkNode(name, bookmarkStarts, endPara)
  200. Else
  201. MoveBookmarkNode(name, bookmarkEnds, endPara)
  202. End If
  203. End If
  204. Next name
  205. Dim hasNodesToRemove As Boolean = True
  206. Do While hasNodesToRemove
  207. hasNodesToRemove = False
  208. Dim i As Integer = 0
  209. Do While i < nodesToRemove.Count
  210. Dim nodeToRemove As Node = CType(nodesToRemove(i), Node)
  211. ‘ Skip already removed nodes.
  212. ‘ Skip nodes that have child nodes.
  213. ‘ Do not remove node if it is the last paragraph in the document.
  214. If Not (nodeToRemove.ParentNode Is Nothing) And _
  215. Not (nodeToRemove.IsComposite AndAlso CType(nodeToRemove, CompositeNode).HasChildNodes) And _
  216. Not (nodeToRemove Is lastParagraph) Then
  217. ‘ Remove node.
  218. nodeToRemove.Remove()
  219. ‘ If at least one node was removed in loop, then the loop will be repeated.
  220. hasNodesToRemove = True
  221. End If
  222. i += 1
  223. Loop
  224. Loop
  225. End Sub
  226. Private Function IsBookmarkNode(ByVal node As Node) As Boolean
  227. Return (node.NodeType = NodeType.BookmarkStart) OrElse (node.NodeType = NodeType.BookmarkEnd)
  228. End Function
  229. Private Function StoreIfBookmark(ByVal bookmarkNames As Hashtable, ByVal bookmarkStarts As Hashtable, ByVal bookmarkEnds As Hashtable, ByVal node As Node) As Boolean
  230. If node.NodeType = NodeType.BookmarkStart Then
  231. Dim bookmarkStart As BookmarkStart = CType(node, BookmarkStart)
  232. bookmarkNames(bookmarkStart.Name) = Nothing
  233. bookmarkStarts.Add(bookmarkStart.Name, bookmarkStart)
  234. Return True
  235. ElseIf node.NodeType = NodeType.BookmarkEnd Then
  236. Dim bookmarkEnd As BookmarkEnd = CType(node, BookmarkEnd)
  237. bookmarkNames(bookmarkEnd.Name) = Nothing
  238. bookmarkEnds.Add(bookmarkEnd.Name, bookmarkEnd)
  239. Return True
  240. End If
  241. Return False
  242. End Function
  243. Private Function RemoveBookmarkNode(ByVal name As String, ByVal bookmarkNodes As Hashtable) As Node
  244. Dim node As Node = CType(bookmarkNodes(name), Node)
  245. node.Remove()
  246. bookmarkNodes.Remove(name)
  247. Return node
  248. End Function
  249. Private Sub MoveBookmarkNode(ByVal name As String, ByVal bookmarkNodes As Hashtable, ByVal para As Paragraph)
  250. para.PrependChild(RemoveBookmarkNode(name, bookmarkNodes))
  251. End Sub
private void RemoveBookmarkWithContent(Bookmark bookmark)
{
        // We need to store other bookmark nodes here, to move them away from the removed area.
        Hashtable bookmarkNames = new Hashtable();
        Hashtable bookmarkStarts = new Hashtable();
        Hashtable bookmarkEnds = new Hashtable();

        ArrayList nodesToRemove = new ArrayList();
        
        BookmarkStart bookmarkStart = bookmark.BookmarkStart;
        BookmarkEnd bookmarkEnd = bookmark.BookmarkEnd;
        Document doc = bookmarkStart.Document;
        Paragraph lastParagraph = doc.LastSection.Body.LastParagraph;

        Node node = SeekFirstNodeOfBookmarkRange(bookmarkStart);

        Paragraph endPara = FindParagraphNextAfterBookmark(bookmarkEnd);

        // Iterate over all nodes that contain or are between bookmark start and end nodes.
        while(node != bookmarkEnd)
        {
                node = node.NextPreOrder(doc);

                // BookmarkStart/BookmarkEnd are saved to be handled separately later.
                // All other nodes are collected as candidates for removal.
                if (!StoreIfBookmark(bookmarkNames, bookmarkStarts, bookmarkEnds, node))
                        nodesToRemove.Add(node);
        }

        foreach (string name in bookmarkNames.Keys)
        {
                if ((bookmarkStarts.ContainsKey(name)) && (bookmarkEnds.ContainsKey(name)))
                {
                        // If bookmark is nested, remove it altogether.
                        RemoveBookmarkNode(name, bookmarkStarts);
                        RemoveBookmarkNode(name, bookmarkEnds);
                }
                else
                {
                        // If bookmark is overlapping, move the contained start/end node to the next paragraph after removed range.
                        if (bookmarkStarts.ContainsKey(name))
                                MoveBookmarkNode(name, bookmarkStarts, endPara);
                        else
                                MoveBookmarkNode(name, bookmarkEnds, endPara);
                }
        }

        bool hasNodesToRemove = true;
        
        while(hasNodesToRemove)
        {
                hasNodesToRemove = false;

                for (int i = 0; i < nodesToRemove.Count; i++)
                {
                        Node nodeToRemove = (Node)nodesToRemove[i];

                        // Skip already removed nodes.
                        if (nodeToRemove.ParentNode == null)
                                continue;

                        // Skip nodes that have child nodes.
                        if (nodeToRemove.IsComposite && (nodeToRemove as CompositeNode).HasChildNodes)
                                continue;

                        // Do not remove node if it is the last paragraph in the document.
                        if (nodeToRemove == lastParagraph)
                                continue;

                        // Remove node.
                        nodeToRemove.Remove();

                        // If at least one node was removed in loop, then the loop will be repeated.
                        hasNodesToRemove = true;
                }
        }
}

private Node SeekFirstNodeOfBookmarkRange(BookmarkStart bookmarkStart)
{
        Node node = bookmarkStart;

        Document doc = node.Document;

        // Bookmark nodes located immediately before start of our bookmark should also be included in the removal process.
        do
        {
                node = node.PreviousPreOrder(doc);
        }
        while(IsBookmarkNode(node));

        // Look back from the bookmark start node to include containing nodes into removal process.
        while(node.IsComposite)
        {
                Node prevNode = node.PreviousPreOrder(doc);

                if (prevNode == null)
                        break;
                else
                        node = prevNode;
        }

        return node;
}

private Paragraph FindParagraphNextAfterBookmark(BookmarkEnd bookmarkEnd)
{
        // Find the paragraph that is next to removed bookmark range.
        // It will be used to move all bookmark start/end nodes belonging to bookmarks overlapping our bookmark,
        // so that they will be preserved after this bookmark removal.
        Paragraph para;

        Node node = bookmarkEnd;
        Document doc = node.Document;

        // It can be that the paragraph containing bookmark end node if the last paragraph in the bookmark range 
        // contains other nodes beside BookmarkEnd or is the last unremovable paragraph in the document.
        if (node.NextSibling != null || node.ParentNode == doc.LastSection.Body.LastParagraph)
        {
                para = (Paragraph)bookmarkEnd.ParentNode;
        }
                // Or it can be the paragraph next to it.
        else
        {
                while(node.NodeType != NodeType.Paragraph)
                {
                        node = node.NextPreOrder(doc);
                }

                para = (Paragraph)node;
        }

        return para;
}

private bool IsBookmarkNode(Node node)
{
        return (node.NodeType == NodeType.BookmarkStart) || (node.NodeType == NodeType.BookmarkEnd);
}

private bool StoreIfBookmark(Hashtable bookmarkNames, Hashtable bookmarkStarts, Hashtable bookmarkEnds, Node node)
{
        if (node.NodeType == NodeType.BookmarkStart)
        {
                BookmarkStart bookmarkStart = (BookmarkStart)node;
                bookmarkNames[bookmarkStart.Name] = null;
                bookmarkStarts.Add(bookmarkStart.Name, bookmarkStart);
                return true;
        }
        else if (node.NodeType == NodeType.BookmarkEnd)
        {
                BookmarkEnd bookmarkEnd = (BookmarkEnd)node;
                bookmarkNames[bookmarkEnd.Name] = null;
                bookmarkEnds.Add(bookmarkEnd.Name, bookmarkEnd);
                return true;
        }

        return false;
}

private Node RemoveBookmarkNode(string name, Hashtable bookmarkNodes)
{
        Node node = (Node)bookmarkNodes[name];
        node.Remove();
        bookmarkNodes.Remove(name);
        return node;
}

private void MoveBookmarkNode(string name, Hashtable bookmarkNodes, Paragraph para)
{
        para.PrependChild(RemoveBookmarkNode(name, bookmarkNodes));
}
[VB .NET] 

Private Sub RemoveBookmarkWithContent(ByVal bookmark As Bookmark)
        ‘ We need to store other bookmark nodes here, to move them away from the removed area.
        Dim bookmarkNames As Hashtable = New Hashtable
        Dim bookmarkStarts As Hashtable = New Hashtable
        Dim bookmarkEnds As Hashtable = New Hashtable

        Dim nodesToRemove As ArrayList = New ArrayList

        Dim bookmarkStart As BookmarkStart = bookmark.BookmarkStart
        Dim bookmarkEnd As BookmarkEnd = bookmark.BookmarkEnd
        Dim node As Node = bookmarkStart
        Dim doc As Document = node.Document
        Dim lastParagraph As Paragraph = doc.LastSection.Body.LastParagraph

        ‘ Bookmark nodes located immediately before start of our bookmark should also be included in the removal process.
        Do
                node = node.PreviousPreOrder(doc)
        Loop While IsBookmarkNode(node)

        ‘ Look back from the bookmark start node to include containing nodes into removal process.
        Do While node.IsComposite
                Dim prevNode As Node = node.PreviousPreOrder(doc)

                If prevNode Is Nothing Then
                        Exit Do
                Else
                        node = prevNode
                End If
        Loop

        ‘ Find the paragraph that is next to removed bookmark range.
        ‘ It will be used to move all bookmark start/end nodes belonging to bookmarks overlapping our bookmark,
        ‘ so that they will be preserved after this bookmark removal.
        Dim endPara As Paragraph

        ‘ It can be the paragraph containing bookmark end node if the last paragraph in the bookmark range 
        ‘ contains other nodes beside BookmarkEnd or is the last unremovable paragraph in the document.
        If Not bookmarkEnd.NextSibling Is Nothing OrElse bookmarkEnd.ParentNode Is lastParagraph Then
                endPara = CType(bookmarkEnd.ParentNode, Paragraph)
                ‘ Or it can be the paragraph next to it.
        Else
                Do While node.NodeType <> NodeType.Paragraph
                        node = node.NextPreOrder(doc)
                Loop

                endPara = CType(node, Paragraph)
        End If

        ‘ Iterate over all nodes that contain or are between bookmark start and end nodes.
        Do While Not node Is bookmarkEnd
                node = node.NextPreOrder(doc)

                ‘ BookmarkStart/BookmarkEnd are saved to be handled separately later.
                ‘ All other nodes are collected as candidates for removal.
                If (Not StoreIfBookmark(bookmarkNames, bookmarkStarts, bookmarkEnds, node)) Then
                        nodesToRemove.Add(node)
                End If
        Loop

        For Each name As String In bookmarkNames.Keys
                If (bookmarkStarts.ContainsKey(name)) AndAlso (bookmarkEnds.ContainsKey(name)) Then
                        ‘ If bookmark is nested, remove it altogether.
                        RemoveBookmarkNode(name, bookmarkStarts)
                        RemoveBookmarkNode(name, bookmarkEnds)
                Else
                        ‘ If bookmark is overlapping, move the contained start/end node to the next paragraph after removed range.
                        If bookmarkStarts.ContainsKey(name) Then
                                MoveBookmarkNode(name, bookmarkStarts, endPara)
                        Else
                                MoveBookmarkNode(name, bookmarkEnds, endPara)
                        End If
                End If
        Next name

        Dim hasNodesToRemove As Boolean = True

        Do While hasNodesToRemove

                hasNodesToRemove = False

                Dim i As Integer = 0

                Do While i < nodesToRemove.Count
                        Dim nodeToRemove As Node = CType(nodesToRemove(i), Node)
                        ‘ Skip already removed nodes.
                        ‘ Skip nodes that have child nodes.
                        ‘ Do not remove node if it is the last paragraph in the document.
                        If Not (nodeToRemove.ParentNode Is Nothing) And _
                                Not (nodeToRemove.IsComposite AndAlso CType(nodeToRemove, CompositeNode).HasChildNodes) And _
                                Not (nodeToRemove Is lastParagraph) Then
                                ‘ Remove node.
                                nodeToRemove.Remove()
                                ‘ If at least one node was removed in loop, then the loop will be repeated.
                                hasNodesToRemove = True
                        End If
                        i += 1
                Loop
        Loop
End Sub

Private Function IsBookmarkNode(ByVal node As Node) As Boolean
        Return (node.NodeType = NodeType.BookmarkStart) OrElse (node.NodeType = NodeType.BookmarkEnd)
End Function

Private Function StoreIfBookmark(ByVal bookmarkNames As Hashtable, ByVal bookmarkStarts As Hashtable, ByVal bookmarkEnds As Hashtable, ByVal node As Node) As Boolean
        If node.NodeType = NodeType.BookmarkStart Then
                Dim bookmarkStart As BookmarkStart = CType(node, BookmarkStart)
                bookmarkNames(bookmarkStart.Name) = Nothing
                bookmarkStarts.Add(bookmarkStart.Name, bookmarkStart)
                Return True
        ElseIf node.NodeType = NodeType.BookmarkEnd Then
                Dim bookmarkEnd As BookmarkEnd = CType(node, BookmarkEnd)
                bookmarkNames(bookmarkEnd.Name) = Nothing
                bookmarkEnds.Add(bookmarkEnd.Name, bookmarkEnd)
                Return True
        End If

        Return False
End Function

Private Function RemoveBookmarkNode(ByVal name As String, ByVal bookmarkNodes As Hashtable) As Node
        Dim node As Node = CType(bookmarkNodes(name), Node)
        node.Remove()
        bookmarkNodes.Remove(name)
        Return node
End Function

Private Sub MoveBookmarkNode(ByVal name As String, ByVal bookmarkNodes As Hashtable, ByVal para As Paragraph)
        para.PrependChild(RemoveBookmarkNode(name, bookmarkNodes))
End Sub

Private Sub MoveBookmarkNode(ByVal name As String, ByVal bookmarkNodes As Hashtable, ByVal para As Paragraph)
para.PrependChild(RemoveBookmarkNode(name, bookmarkNodes))
End Sub