Announcing ZoterZero for author-only cites

Due to personal need, I've created a crude patch to insert author-only cites, for example to pair with 'suppress author' cites. This is far from an ideal solution, but it is useful until Zotero can natively handle this necessary function.

Background information:
(If you like this and want to see a better version implemented in Zotero itself, I would encourage you to voice your support of the proposal there, or to help implement it if you know how.)

1. Purpose
Zotero only allows two output formats for cites:
A) Full, e.g. (Author YYYY)
B) Suppress-author, e.g. (YYYY)
A third variant is needed to allow integration with sentences:
C) Integrated, e.g. Author (YYYY)
Or, more simply, we can build (C) from (B) plus author-only:
D) Author-only, e.g Author
Combining (D) + (B), technically two cites, we would get:
Author (YYYY) (=C)

2. Method and limitations
This crude patch is surface-level in MS Word and modifies the text of cites themselves. It does not interact directly with Zotero.
I have written this for my own purposes, and it functions based on:
A) Word 2011 for Mac.
It may work with other versions or may require adjustments but the outline should apply if you want to adapt it.
B) "Unified style sheet for linguistics journals"
The format of cites is (Author YYYY:pp). Because this text is edited on the surface-level, the code does NOT support other, even similar, variants, like APA's (Author, YYYY:pp) (with a comma). It could, however be easily adapted to work with it, if you want to modify it for your own uses. (This is entirely based on the VBA macro code for Word, not controlled from Zotero's CSL styles.)
C) Manual use: No interaction with Zotero
You must apply it manually to update cites, but I made doing so as effortless as possible (see below), because I plan on using this a lot.
D) Supplied as-is
The code works for me, and I happily release it into public domain for reuse and modification. However, it comes with no warranty or guarantee that it will work. I've done what I can to try to make it harmless and just a cosmetic patch on top of Zotero's default functions, but keep an eye out for any bugs. Feedback is welcome although I can't promise I'll be able to fix it.

3. Usage
1. Citing in Zotero
If you want an author-only citation insert pages as 0. This is a crude trick (and you can see why I picked the name 'ZoterZero'), but it's the only way we have of controlling the output given no other options in Zotero's add/edit citation menu.
In the extremely unlikely event that for some reason you actually need to cite page zero of a text, I suppose the workaround would be to add that manually as a suffix to the cite, e.g., :0
2. Converting to author-only
Run the ZoterZero macro and watch as, e.g., (Author YYYY:0) becomes Author.
The macro will initially try to convert any cites in the current selection (or within close range of the cursor). If no zero-cites are found to be converted, then it will continue for the whole document. This gives you two options:
A) Convert only the current cite, e.g., after inserting or editing it via Zotero. Your cursor should be in or next to the cite field. Or select a range of text anywhere in your document to update, and any cites within that range will be converted.
B) Convert ALL cites in the document, which for very long documents might take some time. You can activate this mode by running the macro twice, the first time converting the selected cite(s), and the next (because those are then already converted) for the whole document. Or this will happen if your cursor/selection is somewhere without any relevant cites.
In short, I recommend running the macro when you insert/edit a citation with Zotero, and also periodically (especially before save/export) on the whole document.
3. Zotero will reset this!
If you edit the cite, or refresh Zotero for the document, all author-only cites will revert back to their full 'page 0' forms. Simply run the ZoterZero macro again to fix this.
Unfortunately you must do this every time because I have found no way to integrate these macros into Zotero's macros. On this limitation, see:

(Code in next post.)
  • edited August 12, 2018
    4. Word macro code
    Function ZoterZeroFieldFix(F) ' Fix a field with ZoterZero
    fieldText = F.Code.Text ' get the current text of the field
    If InStr(fieldText, " ADDIN ZOTERO_ITEM CSL_CITATION") = 1 Then ' make sure this is a Zotero field to modify
    myString = F.Code.Text ' get the code from the field
    myArray = Split(myString, " ", 5) ' split that code into parts up to 5
    Dim Json As Object ' create a Json object to work with
    Set Json = JsonConverter.ParseJson(myArray(4)) ' get the fourth part, which is the Json data
    If Json("citationItems").Count = 1 Then ' only works for one citation item
    If Json("citationItems")(1)("locator") = "0" Then ' if the page range is set to "0" process for ZoterZero:
    myResult = F.Result.Text ' get the displayed text from the field
    If InStr(myResult, "(") = 1 Then ' if it begins with open parentheses...
    myResult = Split(myResult, "(", 2) ' split at parentheses, only 2 parts
    myResult = myResult(1) ' get the second part (without open parentheses)
    myResult = StrReverse(myResult) ' reverse the string so we can:
    myResult = Split(myResult, " ", 2) ' split at spaces, up to 2 parts
    myResult = myResult(1) ' get the second part (without the actually-last [=year] section)
    ' the following two lines make Zotero think this was the original output, so no warnings!
    myResult = StrReverse(myResult) ' reverse back to normal order
    Json("properties")("plainCitation") = myResult ' set the Json citation data to new label
    Json("properties")("formattedCitation") = myResult ' again, other instance
    F.Result.Text = myResult ' replace the displayed text with the next text
    myJson = JsonConverter.ConvertToJson(Json) ' collapse Json back to text
    F.Code.Text = " " & myArray(1) & " " & myArray(2) & " " & myArray(3) & " " & myJson & " " ' reconstruct field code
    ZoterZeroFieldFix = 1 ' updated, return success
    End If
    End If
    End If
    End If
    End Function

    Sub ZoterZero()
    ' ZoterZero main function
    '' if selection or text near cursor contains fields, check and fix them
    '' else check and fix all fields in document
    changeSuccess = 0 ' no fields fixed yet
    Selection.Expand Unit:=wdSentence ' expand the selection to at least sentence-level
    If Selection.Fields.Count > 0 Then ' if fields are selected...
    checkField = Selection.Fields.Count ' get the total number of fields
    While checkField > 0 ' check each field
    changeSuccess = ZoterZeroFieldFix(Selection.Fields(checkField)) ' check and fix this field
    checkField = checkField - 1 ' check the previous field next
    End If
    If changeSuccess = 0 Then ' no fields have been updated yet, let's update all fields in document
    ' based on
    Dim rngStory As Word.Range ' vars for below
    Dim lngValidate As Long ' vars for below
    Dim oShp As Shape ' vars for below
    lngValidate = ActiveDocument.Sections(1).Headers(1).Range.StoryType ' starting point
    For Each rngStory In ActiveDocument.StoryRanges 'Iterate through all linked stories
    On Error Resume Next
    checkField = rngStory.Fields.Count ' get the total number of fields in this section
    While checkField > 0 ' check each field
    changeSuccess = ZoterZeroFieldFix(rngStory.Fields(checkField)) ' check and fix this field
    checkField = checkField - 1 ' check the previous field next
    Select Case rngStory.StoryType
    Case 6, 7, 8, 9, 10, 11
    If rngStory.ShapeRange.Count > 0 Then
    For Each oShp In rngStory.ShapeRange
    If oShp.TextFrame.HasText Then
    checkField = Shp.TextFrame.TextRange.Fields.Count ' get the total number of fields in this section
    While checkField > 0 ' check each field
    changeSuccess = ZoterZeroFieldFix(Shp.TextFrame.TextRange.Fields(checkField)) ' check and fix this field
    checkField = checkField - 1 ' check the previous field next
    End If
    End If
    Case Else 'Do Nothing
    End Select
    On Error GoTo 0
    'Get next linked story (if any)
    Set rngStory = rngStory.NextStoryRange ' get ready for next section
    Loop Until rngStory Is Nothing ' keep going through until all sections are done
    End If
    Selection.Collapse ' reset cursor to beginning of section which isn't quite right but close enough
    End Sub

    How does it work?
    This code searches through fields in Word for any that are Zotero cites, and if the page (stored in "locator") is "0" then it replaces the text with a name-only variant (using surface-level string editing). It does not modify multi-item cites, because it isn't clear what format would be used. (If you have multiple items from the same author, you could add an author-only cite from just one of those items!)
    This code circumvents the Zotero warning about modifying cites, by editing the metadata within those cites to match the author-only text, but does not stop Zotero from updating them, and they will be replaced again with Zotero's default format (including "page 0"). See also comments in the code above for details.

    Installation: you can use this macro in Word as you would any other macro. Personally I have saved it in and saved this in my Word Startup folder, next to where the file is installed by Zotero's Word plugin. I have also added a menu item to the Zotero menu that runs the ZoterZero macro when clicked, and you could assign a keyboard command too.
    Also required: you must also install the VBA-Json parser available here: (follow instructions there, also on the 'VBA-dictionary' it requires)

    Comments, feedback, suggestions, and corrections are all welcome.

    I literally taught myself the basics of VBA (Word's macro scripting language) over the last few days to try to find a solution to my personal need for this function, and there are certainly many ways in which the code could be improved. I'm just happy I finally got something to work.

    (If anyone knows how to create an easy-to-install package for Word, please let me know. I can't offer better installation instructions at this time.)
  • edited September 27, 2019
    In case anyone else is using this (or thinking about something similar), here is an update/expansion to the code:

    I added some features for my own use. The same original functions still work. But three things have been added/changed:

    1. I found a bug when no date was included for an entry (causing a parse error if no space was found in the author, or splitting the author's name if there was a space). Now it just, in effect, removes parentheses (and the ":0") if no year is included. This makes it more robust in case you might add or remove a year from an entry at some point.

    2. I added a shortcut option for page ":00" to generate a single Author (YYYY) cite automatically rather than needing to combine two. This is limited only to that basic functionality: it doesn't work when specifying page numbers (you're using "00" as the command instead!) or when citing multiple items (like above), nor does it allow any modification of the author's name (like possessive 's) or splitting the author and year. For any variants like that just use the original approach with two separate cites, one for Author (via page ":0") and another suppressed-author (YYYY). But since this format comes up often, it's just a shortcut to not slow down Word so much with multiple cites every time.

    3. Remove parentheses: if you insert as a prefix "(" or as a suffix ")" then the resulting doubled parentheses at the edges of any cite will be recognized as a command to remove parentheses. So for example, ((Author YYYY)) will automatically be converted to Author YYYY. This is meant to be used only in the rare cases where you must avoid parentheses-within-parentheses or similar. This also allows you to avoid having overly complex prefixes and suffixes. Personally I found the need especially important when I wanted to insert other Word fields within the same parentheses: the citation in particular that motivated me to add this was roughly: (See also Smith 2018, and discussion in Section 3) where that 3 was a Word cross-reference to another section in the document that cannot be inserted as a suffix to the cite of course. Note that this option does combine with those above, if needed. You can also have only one side (start or end) of the cite "open" without parentheses. Use only when needed, but this should now cover all imaginable usage cases.

    I also cleaned up the code overall a bit.


    Known limitations:

    1. Multi-word "dates" like "In Progress". The code just isn't setup to deal with this, so hopefully it comes up rarely. But in the rare event of (Author In Progress) you would get the erroneous parse Author In (Progress). The code could be expanded to deal with this, but hopefully it's rare enough to not be a problem. Note that you could also get around this by citing some other entry for the same author as the author-only cite, then suppress-author for this one, if you're already citing multiple works by the same author. Awkward, and this is a real bug, but hopefully not a common problem. You can of course edit the citation manually as a last resort.

    2. Capitalization: the only variation not covered above that I have run into in normal usage is capitalization of the author's name. With certain lowercase prefixes like "von" (Dutch or German names), the lowercase form is correct within parentheses and when used in the middle of a sentence. But it should be capitalized at the beginning of a sentence ("Von"), and there is no way to do this automatically at the moment. For now, this will have to be manually modified for such usage. I thought about maybe adding "!" as a prefix-command in author-only cites to automatically capitalize but decided against adding more complexity at the moment. UPDATE: Feature added with an update posted below.

    3. Now perhaps even more than before this is specifically designed for my customized version of the Unified Linguistics stylesheet. I should also note that a recent change to the official Zotero version added a space between year and page, which is NOT found in my version, and therefore will not be compatible with this code. It could be rewritten to expect an extra space, but that isn't currently the case. If you have questions, let me know and I'll try to point you in the right direction, but given how much this relies on the specific output form of cites, it will need to be customized for any individual style.

    The good news is the developers have opened a ticket to add some or all of these features to Zotero itself, so this Word-macro hack may be able to be retired soon. But it's helping me for now, and maybe someone else!
  • edited November 23, 2018
    By request, here is a parentheses-only option. Should work for any style that uses parentheses (it searches for and replaces only doubled parentheses, so I don't expect any errors with other style types).

    As above, there could be some errors/limitations but I think this will work well. It does require the same dependencies as above (the JSON macro, etc.).

    To remove opening parentheses, add "(" as the prefix.
    To remove closing parentheses, add ")" as the suffix.
    (No other functions.)

    This works for multiple-item cites, by adding the prefix to the first item, and/or suffix to the last item.
  • If CSL allowed testing for something like "locator has value", this could be done within a style even, no macros required.
  • edited November 23, 2018
    If CSL allowed testing for something like "locator has [specific] value", this could be done within a style even, no macros required.
    That's an excellent point, and would be a useful addition to CSL in general, although arguably too powerful (I believe why it isn't included now, although I'd rather have the option than none).

    On the one hand, changing CSL to allow a hack for this seems like the wrong solution when adding official commands would be the best solution (already in progress as linked in the other discussion about multiple citation patterns). But on the other hand, having if-statements based on the specific values of terms in CSL comes up as a request fairly often on the forums, such as checking whether a day/month is set in a date and if so displaying it at the end of the cite as the date of a talk, etc. (but not if it would only be the year, duplicating the info at the beginning of the cite).

    CSL does allow checking if a variable is set (to any value at all), but I can't think of any way to work that into the limited insert/edit citation popup to control how cites are displayed. Actually, one very crude way of doing this would be to always manually cite pages as suffixes, and then only insert a "page" (with any 'page number'=locator) in order to show an author-only cite. I don't think anything else in that window would be available to check if it is set, in CSL. Almost...


    Edit: actually... would this work!??
    In the insert/edit cite window, you can click "page" and swap it out for another locator like chapter or various other things. If we picked one we're unlikely to ever use (like "opus"!), could CSL recognize that and then, because that weird type of locator is set, use that as a flag to display the cite differently and get an author-only cite? It's a bizarre workaround, but interesting conceptually.
  • That would totally work. I was thinking about something like testing for "locator sub-verbo is present". Too bad for the sub-verbo crowd though.
  • edited November 23, 2018
    Could different locator types then specify different formats? Interesting. Someone should try this out. It's far from the 'right' way to do it, but exciting there's even a possibility.
  • Quick update: I wasn't sure how compatible this was with different versions of Word, but now for personal reasons I've had to try this with other setups, and it seems to be generally compatible. Confirmed with Word 2010 for Windows, and also Word 365 (current v.16) for Mac. There might be some incompatibility somewhere (newest versions on Windows?) but that wide range of compatibility is encouraging. (Technical note: the uncertainty was due to reading online that functions for making selections of text might vary between Word versions or Mac vs. Windows versions, but that doesn't seem to be a problem.)

    Comments on compatibility are welcome here.
  • Minor Update (ZoterZero4)

    I've added an additional option to force the capitalization of the first letter of a name when it might begin a sentence, such as in the case of "von">"Von" or "de">"De".
    (Inspired by: )

    To use, just add ^ as a prefix, and the first letter of the name will be capitalized. (If you're using the remove-parentheses option with the prefix (, then the combined prefix is (^.)
  • edited July 23, 2021
    @eric_f replying to your request here to make this compatible with APA:

    First, let's define the differences between the style I'm using and APA (see also note above). My style has no space after the colon, so it looks like: (Author YYYY:pp), while APA (and also now the Unified Linguistics style hosted in the Zotero repository) has a space, like this: (Author YYYY: pp).

    In principle, that should be easy to adopt the code, but because I used the space character as a splitting point, we'll need to adjust that a bit. The simplest option seems to be simply repeating the split, so it first splits off the pages (i.e. "0"), then the year, leaving only the author.

    Note that I really know very little about Word macros, and I simply spent hours of trial and error to get this to work in the current version. Each line is commented so you can try to edit it yourself. Here's a suggestion on what I think would work here, but it's untested.

    Find the following section:
    myResult = Split(myResult, splitChar, 2) ' split at splitChar, up to 2 parts
    myResult = myResult(1) ' get the second part (without the actually-last [=year] section)

    Replace that with:
    myResult = Split(myResult, splitChar, 2) ' split at splitChar, up to 2 parts
    myResult = myResult(1) ' get the second part (without the actually-last [=pages] section)
    myResult = Split(myResult, splitChar, 2) ' split at splitChar, up to 2 parts
    myResult = myResult(1) ' get the second part (without the actually-last [=year] section)

    (That just does the same operation twice, splitting off the end of the string that has the pages then the year, while in the original version that was a single piece.)

    If that works, I think something similar can be done for the "00" option to have a simple Author (YYYY) cite, in the next section of the code. I think this might work, but again, untested:
    myResult = Split(myResult, splitChar, 2) ' split at splitChar, up to 2 parts
    myResult = myResult(0) & "(" & splitChar & myResult(1) ' recombine with parentheses inserted
    myResult = Split(myResult, ":", 2) ' split without pages, up to 2 parts
    myResult = ")" & myResult(1) ' recombine without pages

    myResult = Split(myResult, splitChar, 3) ' split at splitChar, up to 3 parts
    myResult = ")" & myResult(1) & "(" & splitChar & myResult(2) ' recombine with parentheses inserted around the year
    myResult = Split(myResult, ":", 2) ' split around the colon, up to 2 parts
    myResult = myResult(0) & myResult(1) ' recombine without colon

    (I think that will work for the most common use cases, but I haven't fully thought through all possible formats with two spaces and whether there might be something that could go wrong with unusually formatted entries.)

    [Note that while this is untested, I don't think it will severely break the code even if something isn't right. It should give you a result to check, and if the formatting isn't quite right you can adjust it as needed.]
Sign In or Register to comment.