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.
  • Hey @gwyn-hopkins,

    here is a 2024 update:

    The line

    from ast import literal_eval as to_dict


    should be replaced by:

    from json import loads as to_dict


    because the "reference marks" are in json format, which causes the original macro to fail on some input (for example if you manually edit some citations).

    Would you mind if I put the latest version on github?
  • Thank you. Yes you have permission to upload to github.

    I did rewrite to automatically detect numbered or author-date references. I am including it here with your update.
    I also wrote something similar for word (https://forums.zotero.org/discussion/86621/linking-citation-to-bibliography#latest) - please note the additional routine I managed to miss off the code I presented in the last comment.


    import uno
    import re
    import json
    from json import loads 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 linkReferences():
    modStyles()
    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)
    data_dictionary = json.loads(reference_mark_data, strict=False)

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

    if plain_cite[0] in "0123456789":
    insertZoteroBookmarks(True)
    plain_cite = plain_cite.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
    else:
    insertZoteroBookmarks(False)

    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 is False:
    break

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



    g_exportedScripts = linkReferences,

  • edited 25 days ago
    Hi @gwyn-hopkins, nice update!

    Just had to use it, and learned a little on how to debug a Libreoffice macro!

    I had to tweak it because some citations caused it to crash: found_range is None when the search text is not found so this causes an AttributeError on the next line.

    I fixed some problems, such as when the citation was edited (in the editor of the "classic" view of zotero's plugin). I put the code at https://gitlab.com/GullumLuvl/bibloids/-/blob/master/LO-zotero-hyperlink-citations.py (Note that I removed the styling parts that I don't use).

    For example, now a citation modified to look like “Smith (1974)” instead of the default “(Smith 1974)” will be processed.

    Remaining problematic citations are:
    • Multiple dates for one author, such as “Smith 1974, 1975”;
    • Manually edited citation text (edited from the main document interface). Then this causes an attribute 'dontUpdate: "true"' to be set, which could be handled.
    • if a prefix was added and is separated by comma, this can trigger comma-splitting a single “author, date” group
    • most often, edited citations cause problems
    Would also be nice to take into account the 'prefix' and 'suffix' fields and keep them out of the hyperlink. Things for my todo list :)

    Thanks again!
  • For pre and post suffixes try adding after "found = false":

    if not citation_year_duplicates:
    citation_year_duplicates = [plain_cite]

    However, this does add them to the hyperlink.
  • Please ignore the previous comment as if does not work.
    Insert this instead which seems to do deal with suffixes, prefixes and page locators (only in English!).
    This should be inserted between
    insertZoteroBookmarks(False)
    and
    if semicolon.search(plain_cite):


    # added Nov 2024 need to remove suffixes etc first
    for citation_items in data_dictionary["citationItems"]:
    label = ""
    if "label" in citation_items:
    label = citation_items["label"]
    locator = ""
    if "locator" in citation_items:
    locator = ", p. " + citation_items["locator"]
    prefix = ""
    if "prefix" in citation_items:
    prefix = citation_items["prefix"] + " "
    suffix = ""
    if "suffix" in citation_items:
    suffix = " " + citation_items["suffix"]
    plain_cite = plain_cite.replace(prefix, "", 1)
    plain_cite = plain_cite.replace(suffix, "", 1)
    plain_cite = plain_cite.replace(locator, "", 1)
Sign In or Register to comment.