Preventing / finding broken internal links to chapter headings

Hello all. I see that when linking to chapter headings in LO Write, it sometimes loses track of which chapter heading it’s supposed to link to. I only found out about this yesterday, but it apparently cannot keep up with much messing around with headings, e.g. moving and renaming headings around the document. I’ll see if I can replicate the issue, but right now I’m a little worried about fixing my own document (+150 pages, hundreds of links).

So:

  1. How may I prevent breaking more links? Is there some behaviour I should be avoiding?
  2. How may I find broken internal links? An extension, a script in any language… please! Alex Kemp is onto something – Utilities to fix Broken Hyperlinks? – but it’s not working well for me. Also, it’d be better if this were a LO add-on, which could take me to where the broken links are; or the next broken link, starting from the current cursor position.

EDIT: I started writing a macro. When generating the list of anchors, I need to access the outline numbering for headings. And I need to open the hyperlink editor on the current text portion.

Option Explicit

' PrintArray displays a MsgBox with the whole array
' for DEBUG purposes only
Sub PrintArray(sTitle as String, theArray() as String)
	Dim sArray, i, iStart, iStop
	sArray = sTitle & ":"
	iStart = LBound(theArray)
	iStop = UBound(theArray)
	If iStart<=iStop then
		For i = iStart to iStop
			sArray = sArray & Chr(13) & theArray(i)
		Next
	End if
	MsgBox(sArray, 64, "***DEBUG")
End sub

' auxiliary sub for BuildAnchorList
Sub AddItemToAnchorList (oAnchors() as String, sTheAnchor as String, sType as String)
	Dim sAnchor
	Select Case sType
		Case "Heading":
			sAnchor = "#" + sTheAnchor + "|outline"
		Case "Table":
			sAnchor = "#" + sTheAnchor + "|table"
		Case "Text Frame":
			sAnchor = "#" + sTheAnchor + "|frame"
		Case "Image":
			sAnchor = "#" + sTheAnchor + "|graphic"
		Case "Object":
			sAnchor = "#" + sTheAnchor + "|ole"
		Case "Section":
			sAnchor = "#" + sTheAnchor + "|region"
		Case "Bookmark":
			sAnchor = "#" + sTheAnchor
	End Select
	ReDim Preserve oAnchors(UBound(oAnchors)+1) as String
	oAnchors(UBound(oAnchors)) = sAnchor
End Sub

' auxiliary sub for BuildAnchorList
Sub AddArrayToAnchorList (oAnchors() as String, oNewAnchors() as String, sType as String)
	Dim i, iStart, iStop
	iStart = LBound(oNewAnchors)
	iStop = UBound(oNewAnchors)
	If iStop < iStart then Exit Sub ' empty array, nothing to do
	For i = iStart to iStop
		AddItemToAnchorList (oAnchors, oNewAnchors(i), sType)
	Next
End Sub

Function BuildAnchorList()
	Dim oDoc as Object, oAnchors() as String
	oDoc = ThisComponent
		
	' get the whole document outline
	Dim oParagraphs, thisPara, oTextPortions, thisPortion
	oParagraphs = oDoc.Text.createEnumeration ' all the paragraphs
	Do While oParagraphs.hasMoreElements
		thisPara = oParagraphs.nextElement
		If thisPara.ImplementationName = "SwXParagraph" then ' is a paragraph
			If thisPara.OutlineLevel>0 Then ' is a heading
				' ***
				' *** TO DO: How do we get the numbering for each heading?
				' For example, if the first level 1 heading text is “Introduction”,
				' the correct anchor is `#1.Introduction|outline`
				' and we are recording `Introduction|outline`
				' ***
				AddItemToAnchorList (oAnchors, thisPara.String, "Heading")
			End if
		End if
	Loop
	' text tables, text frames, images, objects, bookmarks and text sections
	AddArrayToAnchorList(oAnchors, oDoc.getTextTables().ElementNames, "Table")
	AddArrayToAnchorList(oAnchors, oDoc.getTextFrames().ElementNames, "Text Frame")
	AddArrayToAnchorList(oAnchors, oDoc.getGraphicObjects().ElementNames, "Image")
	AddArrayToAnchorList(oAnchors, oDoc.getEmbeddedObjects().ElementNames, "Object")
	AddArrayToAnchorList(oAnchors, oDoc.Bookmarks.ElementNames, "Bookmark")
	AddArrayToAnchorList(oAnchors, oDoc.getTextSections().ElementNames, "Section")
	
	BuildAnchorList = oAnchors
End Function

Function isInArray( theString as String, theArray() as String)
	Dim i, iStart, iStop
	iStart = LBound(theArray)
	iStop = UBound(theArray)
	If iStart<=iStop then
		For i = iStart to iStop
			If theString = theArray(i) then
				isInArray = True
				Exit function
			End if
		Next
	End if
	isInArray = False
End function

Sub FindBrokenInternalLinks
	' Find the next broken internal link
	'
	' Pseudocode:
	'
	' * generate link of anchors - *** TO DO: prefix the outline numbering for headings
	' * loop, searching for internal links
	'     - is the internal link in the anchor list?
	'         * Yes: continue to next link
	'         * No: (broken link found)
	'             - select that link text - *** TO DO: cannot select it
	'             - open link editor so user can fix this
	'             - stop
	' * end loop
	' * display message "No bad internal links found"

	Dim oDoc as Object, oFrame as Object, oDispatcher as Object
	Dim oAnchors() as String ' list of all anchors in the document
	Dim oParagraphs, thisPara, oTextPortions, thisPortion ' for interating through doc
	Dim sMsg ' for MsgBox
	
	oDoc = ThisComponent
	oFrame = ThisComponent.CurrentController.Frame
    oDispatcher = createUnoService("com.sun.star.frame.DispatchHelper")

	' get all document anchors
	oAnchors = BuildAnchorList()
	PrintArray("Anchor list", oAnchors) ' *** DEBUG ***
	
	' find links	
	oParagraphs = oDoc.Text.createEnumeration ' has all the paragraphs
	Dim iLinks
	Dim oViewCursor
	Dim sLink
	iLinks = 0 ' internal link counter

	' go through all the paragraphs
	While oParagraphs.hasMoreElements
		thisPara = oParagraphs.nextElement
		oTextPortions = thisPara.createEnumeration
		' go through all the text portions in current paragraph
		While oTextPortions.hasMoreElements
			thisPortion = oTextPortions.nextElement
			If left(thisPortion.HyperLinkURL, 1) = "#" then
				' internal link found
				iLinks = iLinks + 1
				sLink = thisPortion.HyperLinkURL
				If not isInArray(sLink, oAnchors) then
					' anchor not found
					' *** DEBUG: code below up to MsgBox
					Dim sHas
					If HasUnoInterfaces(thisPortion, "com.sun.star.text.XTextRange") then _
						sHas = "yes" Else sHas = "no"
					sMsg = "Bad link: [" & thisPortion.String & "] -> [" _
						& thisPortion.HyperLinkURL & "] XTextRange interface? " & sHas
					MsgBox (sMsg, 48, "Find broken internal link")
					' ***
					' *** TO DO: How do we open a _specific_ hyperlink for editing?
					' Do we pass parameters to `.uno:EditHyperlink`?
					' Do we move the cursor? (Except all moves I found were relative,
					' e.g. `.uno:GoRight`)
					' Do we use the text portion’s `.Start` and `.End` properties?
					' ***
					' open the current hyperlink for editing
					oDispatcher.executeDispatch(oFrame, ".uno:EditHyperlink", "", 0, Array())
					Exit sub
				End If
			End if
		Wend
	Wend
	If iLinks then
		sMsg = iLinks & " internal links found, all good"
	Else
		sMsg = "This document has no internal links"
	End if
	MsgBox (sMsg, 64, "Find broken internal link")
End Sub

EDIT: I have also asked on Stack Exchange: http://stackoverflow.com/q/37611030/6418653