Scrivener + Quarto + Cite Tools: advanced bibliography for a technical/academic publishing workflow

In another thread, Ian (@nontroppo) generously shared a variation of his main Scrivener template adapted to work with Quarto.

Here, I am sharing another version of that template that has been adapted to work with a new Quarto extension called Cite Tools, that introduces advanced bibliography features to Pandoc and Quarto (previously available only in BibTeX).


The template will make it easier to use multiple bibliographies and cite arbitrary fields of the references.

Multiple bibliographies (with citation backlinks!)

Cite Field

A sample output can be downloaded here. The main changes, that concern Cite Tools and this template, are on page 20 of the PDF.

Dropbox | Permalink

Do it with ease

The Scrivener template will make it much easier to use Cite Tools. Multiple-bibliographies can be set up by simply providing the path to the bib file as a meta-data attribute of the section where it should output.

The sample project can be compiled right away. All of the bibliography files are on Github and will be accessed via the URL (you’ll need an internet connection). BUT, bear in mind, the original project has several dependencies, such as R packages (e.g. ggplot2), apart from xquartz and librsvg. Unless you have these installed, compilation will likely fail unless you leave out sections with R code.

“Do it with style”

Styles will make it easier to cite arbitrary fields from the references.

Side note. Let us use imagine for a moment that these are built-in styles, and that Scrivener is aware of the bibliography files specified above. Wouldn’t it be sweet if it could use its own built-in Citeproc engine to display the rendered citations in the text (according to the style applied)?

Tip #1: Inspecting the bibliography files from Scrivener

Bonus #1: Pandoc-style inline footnotes

Check out the extra style to create Pandoc-style inline footnotes.


Bonus #2: Rolling in the deep

New section type allows the depth of the current binder item to be overridden from the custom metadata attributes. Place the number of hashes in the Depth field and select the appropriate section type.

If you need the Binder and the TOC organized differently, it could come in handy.

Bonus #3 Connected text

The <$linkID> is now being used to create anchors all over the document. This allows us to create links to any binder item (including text parts with no markdown headings)

Changes to the original Ruby Script
  • Changed: the script will now time-out after 90 seconds (easy to change) to prevent indefinite hangs.
  • Changed: the log will open only if the process fails.
  • Fixed: the file paths are now quoted in the conversion command to avoid errors.
Tip #2: Zotero API URL

If you use Zotero, you could even use the API to connect directly to your database using as in The only problem is that you would have to use Zotero’s built-in cite keys (something I find outrageous).

Download Project

Dropbox | Github permalink


Wow, these bibliography updates are just awesome, thanks Bernardo (the backlinks rock!), and thanks for the clear instructions! I will certainly switch to citetools for my main pandoc workflow, do the backlinks work for DOCX, or only PDF?

One mini buglet, on the Conflict of Interests page, there is a rogue “Bibliography”…

1 Like

I’ve just tried this out, but quarto fails with the following (from the end of the log file):

[INFO] Running filter /Applications/quarto/share/filters/quarto-init/quarto-init.lua
[INFO] Completed filter /Applications/quarto/share/filters/quarto-init/quarto-init.lua in 23 ms
[INFO] Running filter /Applications/quarto/share/filters/authors/authors.lua
[INFO] Completed filter /Applications/quarto/share/filters/authors/authors.lua in 14 ms
[INFO] Running filter citetools
Error running filter citetools:
Could not find executable citetools

There was some problem opening /Users/lyndon/Documents/Workflow/Test/quarto/Quarto.qmd/Quarto2.docx, check compiler log…

I’ve definitely installed citetools as an extension (via quarto install extension bcdavasconcelos/citetools) so I’m not sure if I’m missing another step?

1 Like

Lyndon (@lyndondrake), just to be 100% sure, you exported to the same folder where the extension was installed? Have you tried running the exported file directly in terminal?

It does! (Oddly, in the example below, I had to remove the R code sections, as it was causing troubles for Word to startup.)


1 Like

You’re quite right, I hadn’t realised this about Quarto extensions. I have now installed it into the folder I have exported to from Scrivener. This now works beautifully for HTML output.

Word (docx) output though produces a file that Word refuses to open. There’s nothing obvious in the console log. I’ve put a copy of the docx file in Dropbox here:

Also, do you have a minimal .bib file with the six citations? Just so I can work off a common set of files.

1 Like

I am glad to know it is working now. I also love the HTML output for previewing. I am pretty happy to do this without spending up to 4 minutes waiting for the TeX file (using BibTeX) to compile.

I think the sections with R code are causing this. At least it stopped happening over here after I left it out.

All the files with references are in the GitHub repo I shared in the first post. You can access it directly using the link provided on the Bookmarks pane of the inspector of the corresponding bib file (so check the Document Bookmarks of the Workflow section, and you’ll see it.

Perfect, thanks for those - and apologies for not seeing them from the first post!

The other thing I’ve realised I need to do with Quarto is split the document into chapter files. Before I try to write a filter, do you happen to have a suitable one?


Hi, I’m banging my head with this bibliography thing, I want to use scrivener for my degree thesis, but I’m learning, I have zoteron as a bibliography manager, I’m also learning and I don’t understand how to make them work together.
Now you post this, it looks interesting, but if you can explain it to me more slowly, I’m not from this world, I study philosophy and medieval anthropology… a bit far from computers.
Thank you very much for the help, in any case I will continue trying to understand.
I’m encouraged that your quote is from Aristotle.
Thank you

Hola,me estoy dando cabezazos con esto de la bibliografía, quiero usar scrivener para mi tesis de grado, pero estoy aprendiendo, tengo zoteron como gestor de bibliografía también estoy aprendiendo y no entiendo el tema de como hacerlos trabajar juntos.
Ahora tu pones esto, se ve interesante, pero si me lo puedes explicar más lentamente, yo no soy de este mundo, estudio filosofía y antropología medieval… un poco lejos de las computadoras.
Muchas gracias por la ayuda, de todas maneras seguiré tratando de entender.
Me ánima que tu cita sea de Aristóteles.

1 Like

@lyndondrake, apologies for the late reply. Here is a slightly modified version of the main ruby script that will do that just that, so you can turn sections into files. Basically, the script takes the content in between the delimiters and creates new files for them, while also removing them from the main .qmd output file (but not from the .md file). The delimiters are:

<!-- begin_file: "relative/path/to/file.ext" -->
<!-- end_file -->

The script includes a function (which I monkey patched to the string type) that turns Scrivener footnotes into Pandoc Style inline footnotes. This is to avoid leaving any of them behind while splitting files.

I have not tested it on Windows or Linux machines.

#!/usr/bin/env ruby
# encoding: utf-8

# This script rewrites markdown from Scrivener to be compatible with
# the cross-referencing system used by Quarto. It also adds paths for
# LaTeX, python and others so that compilation works directly from
# Scrivener (which by default doesn't use the user environment).
# Version: 0.1.7

Encoding.default_external = Encoding::UTF_8
Encoding.default_internal = Encoding::UTF_8

require 'tempfile' # temp file tools
require 'fileutils' # ruby standard library to deal with files
require 'timeout'

#require 'debug/open_nonstop' # debugger
class String
  def inline_fn(str, style = :pandoc)
    return str unless str.is_a?(String) && !str.empty? && str.include?('[^')

    ref_start = ''
    text = str
    counter = 0

    until ref_start.nil?
      counter += 1
      cite      = "[^#{counter}]"
      ref       = "[^#{counter}]:"
      ref_start = text.index(ref)
      break if ref_start.nil?

      next_ref = "[^#{counter + 1}]:"
      ref_end  = text.index(next_ref).nil? ? -1 : text.index(next_ref) - 2
      offset   = counter.to_s.length + 5
      note     = case style
                 when :mmd
                   "[^#{text[ref_start + offset..ref_end].strip}]"
                   "^[#{text[ref_start + offset..ref_end].strip}]"

      text = text.gsub(cite, note)

    if counter >= 1
      case style
      when :mmd
        text = text.gsub(/\n\s*\[\^/, "\n[^")
        cut_point = text.index("\n[^")
        text = text.gsub(/\n\s*\^\[/, "\n^[")
        cut_point = text.index("\n^")

      text = text[0, cut_point]
      # puts "#{counter -= 1} notes replaced."

  def inline_fn_pandoc
    inline_fn(self, :pandoc)

  def inline_fn_mmd
    inline_fn(self, :mmd)

def makePath() # this method augments our environment path
  home = ENV['HOME'] + '/'
  envpath = ''
  pathtest = [home+'.rbenv/shims', home+'.pyenv/shims', '/usr/local/bin',
              '/usr/local/opt/ruby/bin', '/usr/local/lib/ruby/gems/2.7.0/bin',
              '/Library/TeX/texbin', '/opt/homebrew/bin',
              home+'anaconda/bin', home+'anaconda3/bin', home+'miniconda/bin', home+'miniconda3/bin',
              home+'.cabal/bin', home+'.local/bin']
  pathtest.each { |p| envpath = envpath + ':' + p if }
  envpath.gsub!(/\/{2}/, '/')
  envpath.gsub!(/:{2}/, ':')
  envpath.gsub!(/(^:|:$)/, '')
  ENV['PATH'] = envpath + ':' + ENV['PATH']
  ENV['LANG'] = 'en_GB.UTF-8' if ENV['LANG'].nil? # Just in case we have no LANG, which breaks UTF8 encoding
  puts "--> Modified path: #{ENV['PATH'].chomp}"
end # end makePath()

def isRecent(infile) # checks if a file is less than 3 minutes old
  return false if !File.file?(infile)
  filetime = File.mtime(infile) # modified time
  difftime = - filetime # compare to now
  if difftime <= 180
    return true
    return false

tstart =
in_filename = File.expand_path(ARGV[0])
puts "--> Input Filename: #{in_filename}"
fail "The specified file does not exist!" unless in_filename and File.file?(in_filename)

fileType = ARGV[1]
if fileType.nil? || fileType !~ /(plain|markdown|html|pdf|epub|docx|latex|odt|beamer|revealjs|pptx)/
  fileType = ''

outo_filename = in_filename.gsub(/\.[q]?md$/,".qmd") # output to [name].qmd
to_file ='fix-x-refs') # create a temp file
lineSeparator = "\n"

begin, 'r') do |file|
    text =

    # cosmetic only: remove long runs (4 or more) of newlines

    # This regex puts {#id} onto end of $$ math block lines
    text.gsub!(/\$\$ ?\n\{\#eq/,'$$ {#eq')

    # this finds all reference-link figures with cross-refs and moves
    # the reference down to the reference link
    figID = /^!\[(?<id>\{#fig-.+?\} ?)(?<cap>.+?)\]\[(?<ref>.+?)\]/
    refs = text.scan(figID)
    refs.each {|ref|
      puts "--> Crossref figure details: Label=#{ref[0]} | #{ref[1]} | #{ref[2]}"
      re = Regexp.compile("^(\\[" + ref[2] + "\\]: *)([^{\\n]+)({(.+)})?$")
      mtch = text.match(re)
      label = ref[0].gsub(/\{([^\}]+?)\}/,'\1').strip
      if mtch.nil?
        puts "----> Failed to match #{label} in the references"
      elsif mtch[4].nil?
        text.gsub!(re, '\0 {' + label + '}')
        text.gsub!(re, '\1\2 {' + label + ' \4}')
    # We now need to remove all #{label} from figure captions
    text.gsub!(figID, '![\k<cap>][\k<ref>]')
    text.gsub!(/\[\^fn(\d+)/, '[^\1')
    text = "#{text}\n\n".inline_fn_pandoc
    # New section: Extract and create new files from main file

    # Pattern: <!-- begin_file: "path/to/file.ext" -->`file_content`<!-- end_file -->\n
    if text =~ /<!-- begin_file: "([^"]+)" -->/ # If there are new files to be added
      puts "New files detected!"

      new_files = [] # Array to store new files
      text.scan(/<!-- begin_file: "([^"]+)" -->/) do |match|
        puts "Found new file: #{match[0]}"
        new_files << match[0]

      new_files.each do |file|
          pattern = /<!-- begin_file: "#{file}" -->\n(.+?)\n<!-- end_file -->/m
          file_content = text.scan(pattern)[0][0] # Get the file content
          decrease_heading_cmd = "quarto pandoc -f markdown -t markdown --shift-heading-level-by=-1 --wrap=none"
          # Check if path existis, if not create it
, 'w') { |f| f.write(file_content) }
          `#{decrease_heading_cmd} "#{file}" -o "#{file}"` if file[/\.[q]?md$/]

          puts "File #{file} created!"

          text.gsub!(pattern, '') unless file_content.nil? || file_content.empty?

          # text.gsub!(/^#+ /,"") # Decrease markdown header levels in the file content
          puts "Error: File #{file} not found!"
      # puts text

    to_file.puts text
  to_file.close, outo_filename)

puts "--> Modified File with fixed cross-references: #{outo_filename}"
tend = - tstart
puts "--> Parsing took: " + tend.to_s + "s"

  # Build and run our quarto command
if fileType.empty?
  cmd = "quarto render \"#{outo_filename}\""
  cmd = "quarto render \"#{outo_filename}\" --to #{fileType}"
puts "\n--> Running Command: #{cmd}"

pid = Process.spawn(cmd)
  Timeout.timeout(90) do
    puts 'waiting for the process to end'
    puts 'process finished in time'
rescue Timeout::Error
  puts 'process not finished in time, killing it'
  Process.kill('TERM', pid)
  # open any log file (generated from scrivener's post-processing)
  `open Quarto.log` if File.file?('Quarto.log') and isRecent('Quarto.log')
  # puts %x(#{cmd})

  # now try to open the resultant file
fileType = 'html' if fileType.match(/revealjs|s5|slidous|html5/)
res = outo_filename.gsub(/\.qmd$/, '.' + fileType)
if File.file?(res) && isRecent(res)
  `open "#{res}"`
  puts "There was some problem opening "#{res}", check compiler log…"

Create the File Section Type.

Don’t forget to select it. Also, if the title is empty, it will fail.

Add the markup to the Title Options of a Section Layout.

Now assign the Section Type File to the File Section Layout.

You should be ready to rock.

1 Like

Now this is fully implemented in this template. It has been working all right for me.