Libreoffice/openoffice linking citations to references

I have written the following macros for libreoffice odt files to link citations to a bookmark for the references. The bookmark (called References) has to be currently set manually.


Sub linkReferencesNumbered
refSearch("([0-9]|[1-9][0-9]|[1-9][0-9][0-9])")
End Sub


Sub linkReferencesAuthorDate
refSearch("\p{Lu}[\p{Lu}\p{Ll}'-]*( et al.) [0-9]{4}")
refSearch("\p{Lu}[\p{Lu}\p{Ll}'-]* [0-9]{4}")
refSearch("\p{Lu}[\p{Lu}\p{Ll}'-]* and \p{Lu}[\p{Lu}\p{Ll}'-]* [0-9]{4}")
End Sub

Sub refSearch(sSearchString)
oDoc = ThisComponent
oCursor = oDoc.Text.createTextCursor

oSearch = oDoc.createSearchDescriptor
oSearch.SearchRegularExpression = True
oSearch.SearchString = sSearchString
oFound = oDoc.findFirst(oSearch)

While Not IsNull(oFound)
oCursor = oFound.Text.createTextCursorByRange(oFound)
if isObject(oCursor.Start.ReferenceMark) Then oCursor.HyperLinkURL = "#References"
oFound = oDoc.findNext(oFound, oSearch)
Wend

End Sub
  • sorry. The first macro should be:

    Sub linkReferencesNumbered
    refSearch("([1-9][0-9][0-9]|[1-9][0-9]|[0-9])")
    End Sub
  • II have extensively modified the macro to link (in most cases) directly to the cited reference in the bibliography list. The macros should work for both numbered and author date citations. However, there are some styles were they do not work (i.e., styles that put lines in between the references).

    Sub linkNumberedReferences
    insertZoteroBookmarks(TRUE)
    refSearchnumbered("([1-9][0-9][0-9]|[1-9][0-9]|[0-9])")
    End Sub

    Sub refSearchnumbered(sSearchString)
    oDoc = ThisComponent
    oCursor = oDoc.Text.createTextCursor
    oSearch = oDoc.createSearchDescriptor
    oSearch.SearchRegularExpression = True
    oSearch.SearchString = sSearchString
    oFound = oDoc.findFirst(oSearch)
    While Not IsNull(oFound)
    oCursor = oFound.Text.createTextCursorByRange(oFound)
    if isObject(oCursor.Start.ReferenceMark) Then
    bm = "#_Ref_" & oCursor.String
    oCursor.HyperLinkURL = bm
    End If
    oFound = oDoc.findNext(oFound, oSearch)
    Wend
    End Sub

    Sub linkAuthorDateReferences
    insertZoteroBookmarks(FALSE)
    refSearch("\p{Lu}[\p{Lu}\p{Ll}'-]*( et al.)(,?) [0-9]{4}")
    refSearch("(\p{Lu}[\p{Lu}\p{Ll}'-]* and )?(\p{Lu}[\p{Lu}\p{Ll}'-]*)(,?) [0-9]{4}")
    refSearch("(\p{Lu}[\p{Lu}\p{Ll}'-]*, \p{Lu}[\p{Lu}\p{Ll}'-]*, and \p{Lu}[\p{Lu}\p{Ll}'-]*)(,?) [0-9]{4}")
    End Sub

    Sub refSearch(sSearchString)
    oDoc = ThisComponent
    oCursor = oDoc.Text.createTextCursor
    oSearch = oDoc.createSearchDescriptor
    oSearch.SearchRegularExpression = True
    oSearch.SearchString = sSearchString
    oFound = oDoc.findFirst(oSearch)
    While Not IsNull(oFound)
    oCursor = oFound.Text.createTextCursorByRange(oFound)
    if isObject(oCursor.Start.ReferenceMark) Then
    bm = "#_Ref_" & findFirstWord (oCursor.string)
    oCursor.HyperLinkURL = bm
    End If
    oFound = oDoc.findNext(oFound, oSearch)
    Wend
    End Sub

    Function findFirstWord (oString)
    oReturnString = Split (oString, " ")
    findFirstWord = Replace (oReturnString(0), ",", "")
    End Function

    Function Replace(Source As String, Search As String, NewPart As String)
    Dim Result As String
    Result = join(split(Source, Search), NewPart)
    Replace = Trim(Result)
    End Function

    Sub insertZoteroBookmarks(bNumbered As boolean)
    deleteRefBookmarks
    Dim vSections
    Dim sEventNames 'Array of event types
    Dim sNames
    oDoc = ThisComponent
    oCursor = oDoc.Text.createTextCursor
    vSections = ThisComponent.TextSections()
    sEventNames = vSections.getElementNames()
    For Each sEventName In sEventNames
    If instr (sEventName, "ZOTERO") Then
    oSection = oDoc.getTextSections().getByName(sEventName)
    oSectionAnchor = oSection.getAnchor
    oViewCursor = oDoc.CurrentController.getViewCursor()
    oViewCursor.gotoRange(oSectionAnchor, false)
    oSelection = oDoc.getCurrentSelection (oViewCursor)
    oSel = oSelection.getbyIndex(0)
    iCount=0
    oPE= oSel.createEnumeration()
    Do While oPE.hasMoreElements()
    oPar = oPE.nextElement()
    If oPar.supportsService("com.sun.star.text.Paragraph") Then
    oSecEnum = oPar.createEnumeration()
    Do While oSecEnum.hasMoreElements()
    oParSection = oSecEnum.nextElement()
    If oParSection.TextPortionType = "Text" Then
    bm = ThisComponent.createInstance("com.sun.star.text.Bookmark")
    oCurs = oParSection.getText().createTextCursorByRange(oParSection)
    If bNumbered Then
    iCount = iCount +1
    bm.Name = "_Ref_" & iCount
    Else
    bm.Name = "_Ref_" & findFirstWord (oCurs.string)
    End if
    oDoc.Text.insertTextContent(oCurs, bm, True)
    End If
    Loop
    End If
    Loop
    End If
    Next
    End Sub


    Sub deleteRefBookmarks
    Dim i As Integer, oBookmarks, sBookMarkNames()
    If HasUnoInterfaces( ThisComponent, "com.sun.star.text.XBookmarksSupplier" ) Then
    oBookmarks = ThisComponent.getBookmarks()
    sBookMarkNames = oBookmarks.getElementNames()
    For i = 0 To Ubound( sBookMarkNames )
    If instr(sBookMarkNames(i), "_Ref_") Then
    oBookmarks.getByName( sBookMarkNames( i ) ).dispose()
    End if
    Next
    End If
    End Sub
  • After some comments from a colleague, I have corrected a couple of things (use Sub linkNumberedReferences for numbered references such as Vancover, and Sub linkAuthorDateReferences for author date such as Harvard - adding an additional search for say just author should be easy :

    Sub linkNumberedReferences
    insertZoteroBookmarks(TRUE)
    refSearchnumbered("([1-9][0-9][0-9]|[1-9][0-9]|[0-9])")
    End Sub

    Sub refSearchnumbered(sSearchString)
    oDoc = ThisComponent
    oSearch = oDoc.createSearchDescriptor
    oSearch.SearchRegularExpression = True
    oSearch.SearchString = sSearchString
    oFound = oDoc.findFirst(oSearch)
    While Not IsNull(oFound)
    oCursor = oFound.Text.createTextCursorByRange(oFound)
    if isObject(oCursor.Start.ReferenceMark) Then
    bm = "#_Ref_" & oCursor.String
    oCursor.HyperLinkURL = bm
    End If
    oFound = oDoc.findNext(oFound, oSearch)
    Wend
    End Sub

    Sub linkAuthorDateReferences
    insertZoteroBookmarks(FALSE)
    refSearch("\p{Lu}[\p{Lu}\p{Ll}'-]*( et al.)(,?) [0-9]{4}")
    refSearch("(\p{Lu}[\p{Lu}\p{Ll}'-]* and )?(\p{Lu}[\p{Lu}\p{Ll}'-]*)(,?) [0-9]{4}")
    refSearch("(\p{Lu}[\p{Lu}\p{Ll}'-]*, \p{Lu}[\p{Lu}\p{Ll}'-]*, and \p{Lu}[\p{Lu}\p{Ll}'-]*)(,?) [0-9]{4}")
    End Sub

    Sub refSearch(sSearchString)
    oDoc = ThisComponent
    oSearch = oDoc.createSearchDescriptor
    oSearch.SearchRegularExpression = True
    oSearch.SearchString = sSearchString
    oFound = oDoc.findFirst(oSearch)
    While Not IsNull(oFound)
    oCursor = oFound.Text.createTextCursorByRange(oFound)
    if isObject(oCursor.Start.ReferenceMark) Then
    bm = "#_Ref_" & findFirstWord (oCursor.string)
    oCursor.HyperLinkURL = bm
    End If
    oFound = oDoc.findNext(oFound, oSearch)
    Wend
    End Sub

    Function findFirstWord (oString)
    oReturnString = Split (oString, " ")
    findFirstWord = Replace (Replace (oReturnString(0), ",", ""), Chr(10), "")
    End Function

    Function Replace(Source As String, Search As String, NewPart As String)
    Dim Result As String
    Result = join(split(Source, Search), NewPart)
    Replace = Trim(Result)
    End Function

    Sub insertZoteroBookmarks(bNumbered As boolean)
    deleteRefBookmarks
    Dim vSections
    Dim sEventNames 'Array of event types
    Dim sNames
    oDoc = ThisComponent
    vSections = ThisComponent.TextSections()
    sEventNames = vSections.getElementNames()
    For Each sEventName In sEventNames
    If instr (sEventName, "ZOTERO") Then Exit For
    Next
    oSection = oDoc.getTextSections().getByName(sEventName)
    oSectionAnchor = oSection.getAnchor
    oCursor = oSectionAnchor.getText().createTextCursorByRange(oSectionAnchor)
    oPE = oCursor.createEnumeration()
    iCount = 0
    Do While oPE.hasMoreElements()
    oPar = oPE.nextElement()
    If oPar.supportsService("com.sun.star.text.Paragraph") Then
    iCount = iCount +1
    bm = ThisComponent.createInstance("com.sun.star.text.Bookmark")
    oCurs = oPar.getText().createTextCursorByRange(oPar)
    If bNumbered Then
    bm.Name = "_Ref_" & iCount
    Else
    bm.Name = "_Ref_" & findFirstWord (oCurs.string)
    End if
    If Not oDoc.getBookmarks().hasByName(bm.Name) Then oDoc.Text.insertTextContent(oCurs, bm, True)
    End If
    Loop
    End sub

    Sub deleteRefBookmarks
    Dim i As Integer, oBookmarks, sBookMarkNames()
    If HasUnoInterfaces( ThisComponent, "com.sun.star.text.XBookmarksSupplier" ) Then
    oBookmarks = ThisComponent.getBookmarks()
    sBookMarkNames = oBookmarks.getElementNames()
    For i = 0 To Ubound( sBookMarkNames )
    If instr(sBookMarkNames(i), "_Ref_") Then
    oBookmarks.getByName( sBookMarkNames( i ) ).dispose()
    End if
    Next
    End If
    End Sub
  • edited July 17, 2021
    Hi,

    as I am not familiar with LO Basic nor with Macros at all, it took me some time to get the things working. Let me drop here some comments for potential other „noobs“:

    -citations in the document have to be as ReferenceMarks (option under „Set Document Preferences“ button: „ReferenceMarks“/„Bookmarks“)

    -Out of all the „routines“/„Subs“ that appear in the list of macros, run only one: „linkNumberedReferences“ or „linkAuthorDateReferences“, based on the actual citation style used in Your document (other „Subs“ are helping functions for organizing the source code)

    -After Macro is run, the numbers marking the references in text (e.g. [27]) should get underlined to symbolise that they became hyperlinks. But I observed also that nothing visible happened which is some bug of my LO distribution (6.2.8.2. x64). Try to export PDF to see, whether the references in text changed into clickable hyperlinks

    -The references does not become clickable hyperlinks in your ODT, it works only in exported PDF (when you hover your mouse over the reference in our ODT, you still can see the „ZOTERO“ system ToolTipText)

    -You don't do the Unlink Citations as it would break also the result of the Macro

    Finally, many thanks to the author of this macro!
  • Thank you for your comments.
    I have updated the libreoffice macro, mainly by converting from the regex searches to find the citations.

    import uno
    import re
    from ast import literal_eval as to_dict

    #alter this for citations divided by comma

    semicolon = re.compile(r"\d\w?;")
    comma = re.compile(r"\d\w?,")
    colon = re.compile(r"\d\w?:")

    document = XSCRIPTCONTEXT.getDocument()

    def modifyStyle(cStyleName, cStyleFamily, oFont, oSize, oColor):
    oStyleFamily = document.getStyleFamilies().getByName( cStyleFamily )
    oStyle = oStyleFamily.getByName( cStyleName )
    oStyle.setPropertyValue("CharFontName", oFont)
    oStyle.setPropertyValue("CharColor", oColor)
    oStyle.setPropertyValue("CharHeight", oSize)
    oStyle.setPropertyValue("CharUnderline", 0)
    oStyle.setPropertyValue("CharNoHyphenation", True)

    def modStyles():
    modifyStyle("Internet Link", "CharacterStyles", "Times New Roman", 12, 0x0000FF)
    modifyStyle("Visited Internet Link", "CharacterStyles", "Times New Roman", 12, 0x0000FF)


    def deleteRefBookmarks():
    bookmarks = document.getBookmarks()
    bmList = []
    for bookmark in bookmarks:
    if "Ref_" in bookmark.Name:
    bmList.append(bookmark.Name)
    for bm in bmList:
    bookmarks.getByName(bm).dispose()

    def insertZoteroBookmarks(bNumbered):
    deleteRefBookmarks()
    vSections = document.getTextSections()
    sEventNames = vSections.getElementNames()
    for sEventName in sEventNames:
    if "ZOTERO" in sEventName:
    oSection = document.getTextSections().getByName(sEventName)
    oSectionTextRange = oSection.getAnchor()
    oPE = oSectionTextRange.createEnumeration()
    iCount = 0
    while oPE.hasMoreElements():
    oPar = oPE.nextElement()
    if oPar.supportsService("com.sun.star.text.Paragraph"):
    iCount = iCount +1
    bm = document.createInstance("com.sun.star.text.Bookmark")
    oCurs = oPar.getText().createTextCursorByRange(oPar)
    if bNumbered:
    bm.Name = "Ref_" + str(iCount)
    else:
    bm.Name = "Ref_" + findFirstWord (oCurs.getString()) + "_" + literatureDate(oCurs.getString())
    if not document.getBookmarks().hasByName(bm.Name):
    document.Text.insertTextContent(oCurs, bm, True)

    def findFirstWord (oString):
    #finds first capitilzed name! updated 2021-09-24
    notFound = True
    while notFound:
    (firstWord, nextString) = oString.split(maxsplit=1)
    if firstWord[0].isupper():
    notFound = False
    else:
    oString = nextString
    return firstWord.rstrip(",")

    def literatureDate(oString):
    year = re.search(r'((19|2[0-9])\d{2}[a-z]?)', oString).group(1)
    # year numbers from 1900 to 2999 and optional a-z
    return year

    def linkAuthorDateReferences():
    modStyles()
    insertZoteroBookmarks(False)
    author_date_references()

    def author_date_references():

    search_descriptor = document.createSearchDescriptor()
    search_descriptor.SearchRegularExpression = True
    text_cursor = document.getText().createTextCursor()

    reference_marks = document.ReferenceMarks

    for reference_mark in reference_marks:
    reference_mark_content = reference_mark.Name
    reference_mark_content, _ = reference_mark_content.rsplit(" ", 1)
    _,_, reference_mark_data = reference_mark_content.split(" ", 2)

    data_dictionary = to_dict(reference_mark_data)

    plain_cite = data_dictionary["properties"]["plainCitation"]
    plain_cite = plain_cite.replace("(", "").replace(")", "")

    if semicolon.search(plain_cite):
    names = list(plain_cite.split(";"))
    elif comma.search(plain_cite):
    names = list(plain_cite.split(","))
    elif colon.search(plain_cite):
    names = list(plain_cite.split(":"))
    else:
    names = [plain_cite]

    for citation_items in data_dictionary["citationItems"]:

    if "itemData" in citation_items.keys():
    item_data = citation_items["itemData"]

    author = item_data["author"][0]["family"]
    date_year = item_data["issued"]["date-parts"][0][0]

    citation = list(filter(lambda x: author in x,names))
    citation_year_duplicates= [c for c in citation if (date_year in c)]

    text_cursor = reference_mark.getAnchor()
    text_cursor_start = text_cursor.getStart()

    found = False
    for cite in citation_year_duplicates:
    search_descriptor.SearchString = cite
    found_range = document.findNext(text_cursor_start, search_descriptor)
    if found_range.HyperLinkURL == "":
    found = True
    if found_range.String[-1].isalpha():
    date_year = date_year + found_range.String[-1]
    break
    else:
    text_cursor_start = found_range.getEnd()

    if found == False:
    break

    text_cursor_by_range = found_range.Text.createTextCursorByRange(found_range)
    text_cursor_by_range.HyperLinkURL = "#Ref_" + author + "_" + date_year

    def linkNumberedReferences():
    modStyles()
    insertZoteroBookmarks(True)
    numbered_references()

    def numbered_references():

    search_descriptor = document.createSearchDescriptor()
    search_descriptor.SearchRegularExpression = True
    text_cursor = document.getText().createTextCursor()

    reference_marks = document.ReferenceMarks

    for reference_mark in reference_marks:
    reference_mark_content = reference_mark.Name
    reference_mark_content, _ = reference_mark_content.rsplit(" ", 1)
    _,_, reference_mark_data = reference_mark_content.split(" ", 2)

    data_dictionary = to_dict(reference_mark_data)

    plain_cite = data_dictionary["properties"]["plainCitation"]
    plain_cite = plain_cite.replace("(", "").replace(")", "").replace("-", ",").replace("–", ",")

    numbers = list(plain_cite.split(","))

    for number in numbers:
    text_cursor = reference_mark.getAnchor()
    text_cursor_start = text_cursor.getStart()
    search_descriptor.SearchString = number
    found_range = document.findNext(text_cursor_start, search_descriptor)
    text_cursor_by_range = found_range.Text.createTextCursorByRange(found_range)

    text_cursor_by_range.HyperLinkURL = "#Ref_" + number


    g_exportedScripts = linkNumberedReferences, linkAuthorDateReferences,

  • You mentioned that the reference numbers do not get underlined. This is true, they should change colour to blue to show that they can be selected. I have not made a change as underlining can look a bit messy. However, the change can be made in: "def modifyStyle" by changing 0 to 1 in this line e.g. oStyle.setPropertyValue("CharUnderline", 1)
    Also, you should be able to use the hyperlinks in libreoffice. You may need to press ctrl at the same time as the mouse button, depends on your setting.
  • Hi, I am a new user of LibreOffice. I want to know how to import and run your python macro in my file. Thank you!
  • Thanks @damnation! The video is very helpful!
    Now i can run both python macro and Basic macro. The python macro didn't change the file (no new bookmarks) while the Basic macro can successfully generate bookmarks for each reference. Would it be possible to hyperlink the citations in the paper content with each bookmark in the reference section?
    I'm not entirely certain whether the macro is intended to perform this function. Would you please be able to advise me on how to resolve this issue?
  • Thanks for the great work, gwyn-hopkins! This feature should really be part of Zotero itself.

    Because the video was not useful at all, here is how install this macro (LibreOffice version 7.3):

    1. On Ubuntu, an extra package should be installed to manage Python macros:
    sudo apt install libreoffice-script-provider-python

    This will create the menu entry "Python" under "Tools > Macros > Manage Macros"

    2. Copy the script into $HOME/.config/libreoffice/4/user/Scripts/python/. For other OS, see here: https://help.libreoffice.org/7.3/en-US/text/sbasic/python/python_locations.html

    Now the script shows up in "run macro" window in "My macros".


    Some comments on the code: the function 'ModStyles' to update character formatting of category "Internet Link" failed, because my interface is in French so I think it should be "Lien Internet". Anyway I removed this function call and edited this style in the normal LibreOffice interface.
    It would be better to use a new specific style for bibliography links, because we want to style them separately from actual internet links.
Sign In or Register to comment.