-- mainstateinit.lua
-- initializes global state for the main.lua script
--
-- Copyright (C) 2022 by RStudio, PBC

-- global state
preState = {
  usingBookmark = false,
  usingTikz = false,
  results = {
    resourceFiles = pandoc.List({}),
    inputTraits = {}
  },
  file = nil,
  appendix = false,
  fileSectionIds = {},
  emulatedNodeHandlers = {}
}

postState = {
}
-- customnodes.lua
-- support for custom nodes in quarto's emulated ast
-- 
-- Copyright (C) 2022 by RStudio, PBC

local handlers = {}

local custom_node_data = pandoc.List({})
local n_custom_nodes = 0

function resolve_custom_node(node)
  if type(node) == "userdata" and node.t == "Plain" and #node.content == 1 and node.content[1].t == "RawInline" and node.content[1].format == "QUARTO_custom" then
    return node.content[1]
  end
  if type(node) == "userdata" and node.t == "RawInline" and node.format == "QUARTO_custom" then
    return node
  end
end

function run_emulated_filter(doc, filter, top_level)
  if filter._is_wrapped then
    return doc:walk(filter)
  end

  local wrapped_filter = {}
  for k, v in pairs(filter) do
    wrapped_filter[k] = v
  end

  local function process_custom_inner(raw)
    _quarto.ast.inner_walk(raw, wrapped_filter)
  end

  local function process_custom_preamble(custom_data, t, kind, custom_node)
    if custom_data == nil then
      return nil
    end
    local node_type = {
      Block = "CustomBlock",
      Inline = "CustomInline"
    }
    local filter_fn = filter[t] or filter[node_type[kind]] or filter.Custom

    if filter_fn ~= nil then
      return filter_fn(custom_data, custom_node)
    end
  end

  local function process_custom(custom_data, t, kind, custom_node)
    local result, recurse = process_custom_preamble(custom_data, t, kind, custom_node)
    if filter.traverse ~= "topdown" or recurse ~= false then
      if tisarray(result) then
        ---@type table<number, table|pandoc.Node>
        local array_result = result ---@diagnostic disable-line
        local new_result = {}
        for i, v in ipairs(array_result) do
          if type(v) == "table" then
            new_result[i] = quarto[t](v) --- create new custom object of the same kind as passed and recurse.
          else
            new_result[i] = v
          end
          process_custom_inner(new_result[i])
        end
        return new_result, recurse
        
      elseif type(result) == "table" then
        local new_result = quarto[t](result)
        process_custom_inner(new_result or custom_node)
        return new_result, recurse
      elseif result == nil then
        process_custom_inner(custom_node)
        return nil, recurse
      else
        -- something non-custom was returned, we just send it along.
        return result, recurse
      end
    else
      -- non-recursing traversal
      if tisarray(result) then
        local new_result = {}
        for i, v in ipairs(result) do
          if type(v) == "table" then
            new_result[i] = quarto[t](v) --- create new custom object of the same kind as passed.
          else
            new_result[i] = v
          end
        end
        return new_result, recurse
      elseif type(result) == "table" then
        local new_result = quarto[t](result)
        return new_result, recurse
      else
        return result, recurse
      end
    end
  end

  local custom = resolve_custom_node(doc)
  if custom then
    local custom_data, t, kind = _quarto.ast.resolve_custom_data(custom)
    local result, recurse = process_custom(custom_data, t, kind, custom)
    if result == nil then
      return doc
    end
    return result, recurse
  end

  function wrapped_filter.Plain(node)
    local custom = resolve_custom_node(node)

    if custom then
      local custom_data, t, kind = _quarto.ast.resolve_custom_data(custom)
      -- only follow through if node matches the expected kind
      if kind == "Block" then
        return process_custom(custom_data, t, kind, custom)
      else
        return nil
      end
    else
      if filter.Plain ~= nil then
        return filter.Plain(node)
      else
        return nil
      end
    end
  end

  function wrapped_filter.RawInline(node)
    local custom = resolve_custom_node(node)

    if custom then
      local custom_data, t, kind = _quarto.ast.resolve_custom_data(custom)
      -- only follow through if node matches the expected kind
      if kind == "Inline" then
        return process_custom(custom_data, t, kind, custom)
      else
        return nil
      end
    else
      if filter.RawInline ~= nil then
        return filter.RawInline(node)
      else
        return nil
      end
    end
  end

  wrapped_filter._is_wrapped = true

  local result, recurse = doc:walk(wrapped_filter)
  if top_level and filter._filter_name ~= nil then
    add_trace(result, filter._filter_name)
  end
  return result, recurse
end

function create_emulated_node(t, tbl, context)
  n_custom_nodes = n_custom_nodes + 1
  local result = pandoc.RawInline("QUARTO_custom", tostring(t .. " " .. n_custom_nodes .. " " .. context))
  custom_node_data[n_custom_nodes] = tbl
  tbl.t = t -- set t always to custom ast type
  return result
end

_quarto.ast = {
  custom_node_data = custom_node_data,

  -- this is used in non-lua filters to handle custom nodes
  reset_custom_tbl = function(tbl)
    custom_node_data = tbl
    n_custom_nodes = #tbl
  end,

  resolve_custom_data = function(raw_or_plain_container)
    if type(raw_or_plain_container) ~= "userdata" then
      error("Internal Error: resolve_custom_data called with non-pandoc node")
      error(type(raw_or_plain_container))
      crash_with_stack_trace()
    end
    local raw

    if raw_or_plain_container.t == "RawInline" then
      raw = raw_or_plain_container
    elseif raw_or_plain_container.t == "Plain" and #raw_or_plain_container.content == 1 and raw_or_plain_container.content[1].t == "RawInline" then
      raw = raw_or_plain_container.content[1]
    else
      return nil
    end

    if raw.format ~= "QUARTO_custom" then
      return
    end

    local parts = split(raw.text, " ")
    local t = parts[1]
    local n = tonumber(parts[2])
    local kind = parts[3]
    local handler = _quarto.ast.resolve_handler(t)
    if handler == nil then
      error("Internal Error: handler not found for custom node " .. t)
      crash_with_stack_trace()
    end
    local custom_node = _quarto.ast.custom_node_data[n]
    return custom_node, t, kind
  end,
  
  add_handler = function(handler)
    local state = (preState or postState).extendedAstHandlers
    if type(handler.constructor) == "nil" then
      print("Internal Error: extended ast handler must have a constructor")
      quarto.utils.dump(handler)
      crash_with_stack_trace()
    elseif type(handler.class_name) == "nil" then
      print("ERROR: handler must define class_name")
      quarto.utils.dump(handler)
      crash_with_stack_trace()
    elseif type(handler.class_name) == "string" then
      state.namedHandlers[handler.class_name] = handler
    elseif type(handler.class_name) == "table" then
      for _, name in ipairs(handler.class_name) do
        state.namedHandlers[name] = handler
      end
    else
      print("ERROR: class_name must be a string or an array of strings")
      quarto.utils.dump(handler)
      crash_with_stack_trace()
    end

    quarto[handler.ast_name] = function(...)
      local tbl = handler.constructor(...)
      return create_emulated_node(handler.ast_name, tbl, handler.kind), tbl
    end

    -- we also register them under the ast_name so that we can render it back
    state.namedHandlers[handler.ast_name] = handler
  end,

  resolve_handler = function(name)
    local state = (preState or postState).extendedAstHandlers
    if state.namedHandlers ~= nil then
      return state.namedHandlers[name]
    end
    return nil
  end,

  inner_walk = function(raw, filter)
    if raw == nil then
      return nil
    end
    local custom_data, t, kind = _quarto.ast.resolve_custom_data(raw)    
    local handler = _quarto.ast.resolve_handler(t)
    if handler == nil then
      if type(raw) == "userdata" then
        return raw:walk(filter)
      end
      print(raw)
      error("Internal Error: handler not found for custom node " .. (t or type(t)))
      crash_with_stack_trace()
    end

    if handler.inner_content ~= nil then
      local new_inner_content = {}
      local inner_content = handler.inner_content(custom_data)

      for k, v in pairs(inner_content) do
        local new_v = run_emulated_filter(v, filter)
        if new_v ~= nil then
          new_inner_content[k] = new_v
        end
      end
      handler.set_inner_content(custom_data, new_inner_content)
    end
  end,

  walk = run_emulated_filter,

  writer_walk = function(doc, filter)
    local old_custom_walk = filter.Custom
    local function custom_walk(node, raw)
      local handler = quarto._quarto.ast.resolve_handler(node.t)
      if handler == nil then
        error("Internal Error: handler not found for custom node " .. node.t)
        crash_with_stack_trace()
      end
      -- ensure inner nodes are also rendered
      quarto._quarto.ast.inner_walk(raw, filter)
      local result = handler.render(node)
      return quarto._quarto.ast.writer_walk(result, filter)
    end

    if filter.Custom == nil then
      filter.Custom = custom_walk
    end

    local result = run_emulated_filter(doc, filter)
    filter.Custom = old_custom_walk
    return result
  end
}

quarto._quarto = _quarto

function constructExtendedAstHandlerState()
  local state = {
    namedHandlers = {},
  }

  if preState ~= nil then
    preState.extendedAstHandlers = state
  end
  if postState ~= nil then
    postState.extendedAstHandlers = state
  end

  for _, handler in ipairs(handlers) do
    _quarto.ast.add_handler(handler)
  end
end

constructExtendedAstHandlerState()
-- emulatedfilter.lua
-- creates lua filter loaders to support emulated AST
--
-- Copyright (C) 2022 by RStudio, PBC

local function plain_loader(handlers)
  local function wrapFilter(handler)
    local wrappedFilter = {}
    wrappedFilter.scriptFile = handler.scriptFile
    for k, v in pairs(handler) do
      wrappedFilter[k] = v.handle
    end
    return wrappedFilter
  end
  return map_or_call(wrapFilter, handlers)
end

make_wrapped_user_filters = function(filterListName)
  local filters = {}
  for _, v in ipairs(param("quarto-filters")[filterListName]) do
    if (type(v) == "string" and string.match(v, ".lua$") == nil) then
      v = {
        path = v,
        type = "json"
      }
    end
    local wrapped = makeWrappedFilter(v, plain_loader)
    if tisarray(wrapped) then
      for _, innerWrapped in ipairs(wrapped) do
        table.insert(filters, innerWrapped)
      end
    else
      table.insert(filters, wrapped)
    end
  end
  return filters
end
-- parse.lua
-- convert custom div inputs to custom nodes
--
-- Copyright (C) 2022 by RStudio, PBC

local function parse(node)
  for _, class in ipairs(node.attr.classes) do
    local tag = pandoc.utils.stringify(class)
    local handler = _quarto.ast.resolve_handler(tag)
    if handler ~= nil then
      return handler.parse(node)
    end
  end
  return node
end

function parseExtendedNodes() 
  return {
    Div = parse,
    Span = parse,
  }
end
-- render.lua
-- convert custom nodes to their final representation
--
-- Copyright (C) 2022 by RStudio, PBC

function render_raw(raw)
  local parts = split(raw.text)
  local t = parts[1]
  local n = tonumber(parts[2])
  local handler = _quarto.ast.resolve_handler(t)
  if handler == nil then
    error("Internal Error: handler not found for custom node " .. t)
    crash_with_stack_trace()
  end
  local customNode = _quarto.ast.custom_node_data[n]
  return handler.render(customNode)
end

function renderExtendedNodes()
  if string.find(FORMAT, ".lua$") then
    return {} -- don't render in custom writers, so we can handle them in the custom writer code.
  end

  local filter -- beware, it can't be a local initialization because of the recursive call to inner_walk
  filter = {
    Custom = function(node, raw)
      local handler = _quarto.ast.resolve_handler(node.t)
      if handler == nil then
        error("Internal Error: handler not found for custom node " .. node.t)
        crash_with_stack_trace()
      end
      _quarto.ast.inner_walk(raw, filter)
      return handler.render(node)
    end
  }

  return filter
end
-- runemulation.lua
-- run filters in pandoc emulation mode
--
-- Copyright (C) 2022 by RStudio, PBC

local function run_emulated_filter_chain(doc, filters, afterFilterPass)
  init_trace(doc)
  if tisarray(filters) then
    for i, v in ipairs(filters) do
      local function callback()
        doc = run_emulated_filter(doc, v, true)
      end
      if v.scriptFile then
        _quarto.withScriptFile(v.scriptFile, callback)
      else
        callback()
      end
      if afterFilterPass then
        afterFilterPass()
      end
    end
  elseif type(filters) == "table" then
    doc = run_emulated_filter(doc, filters, true)
    if afterFilterPass then
      afterFilterPass()
    end
  else
    error("Internal Error: run_emulated_filter_chain expected a table or array instead of " .. type(filters))
    crash_with_stack_trace()
  end
  end_trace()
  return doc
end

local function emulate_pandoc_filter(filters, afterFilterPass)
  return {
    traverse = 'topdown',
    Pandoc = function(doc)
      local result
      -- local profiling = true
      if profiling then
        local profiler = require('profiler')
        profiler.start()
        -- doc = to_emulated(doc)
        doc = run_emulated_filter_chain(doc, filters, afterFilterPass)
        -- doc = from_emulated(doc)

        -- the installation happens in main.lua ahead of loaders
        -- restore_pandoc_overrides(overrides_state)

        -- this call is now a real pandoc.Pandoc call
        profiler.stop()

        profiler.report("profiler.txt")
        crash_with_stack_trace() -- run a single file for now.
      end
      return run_emulated_filter_chain(doc, filters, afterFilterPass), false
    end
  }
end

function run_as_extended_ast(specTable)
  local pandocFilterList = {}
  if specTable.pre then
    for _, v in ipairs(specTable.pre) do
      table.insert(pandocFilterList, v)
    end
  end

  table.insert(pandocFilterList, emulate_pandoc_filter(specTable.filters, specTable.afterFilterPass))
  if specTable.post then
    for _, v in ipairs(specTable.post) do
      table.insert(pandocFilterList, v)
    end
  end

  return pandocFilterList
end
-- traceexecution.lua
-- produce a json file from filter chain execution
--
-- Copyright (C) 2022 by RStudio, PBC

local data = {}

if os.getenv("QUARTO_TRACE_FILTERS") then
  function init_trace(doc)
    table.insert(data, {
      state = "__start",
      doc = quarto.json.decode(pandoc.write(doc, "json"))
    })
  end

  function add_trace(doc, filter_name)
    table.insert(data, {
      state = filter_name,
      doc = quarto.json.decode(pandoc.write(doc, "json"))
    })
  end

  function end_trace()
    local file = io.open("quarto-filter-trace.json", "w")
    if file == nil then
      crash_with_stack_trace()
      return
    end
    file:write(quarto.json.encode({
      data = data
    }))
    file:close()
  end
else
  function init_trace(doc)
  end
  function add_trace(doc, filter_name)
  end
  function end_trace()
  end
end
-- wrappedwriter.lua
-- support for creating better custom writers
--
-- Copyright (C) 2022 by RStudio, PBC

function wrapped_writer()
  return filterIf(function()
    return param("custom-writer")
  end, makeWrappedFilter(param("custom-writer"), function(handler)
  
    local resultingStrs = {}
  
    local contentHandler = function(el)
      return el.content
    end
  
    local bottomUpWalkers = {
      Pandoc = function(doc)
        local result = {}
        if doc.blocks then
          for _, block in ipairs(doc.blocks) do
            table.insert(result, block)
          end
        end
        -- TODO I think we shouldn't walk meta, but I'm not positive.
        -- if doc.meta then
        --   table.insert(result, doc.meta)
        -- end
        return result
      end,
      BlockQuote = contentHandler,
      BulletList = contentHandler,
  
      DefinitionList = contentHandler,
  
      Div = contentHandler,
      Header = contentHandler,
      LineBlock = contentHandler,
      OrderedList = contentHandler,
      Para = contentHandler,
      Plain = contentHandler,
  
      Cite = function(element)
        local result = {}
        for _, block in ipairs(element.content) do
          table.insert(result, block)
        end
        for _, block in ipairs(element.citations) do
          table.insert(result, block)
        end
        return result
      end,
  
      Emph = contentHandler,
      Figure = function(element)
        local result = {}
        for _, block in ipairs(element.content) do
          table.insert(result, block)
        end
        table.insert(result.caption)
        return result
      end,
      Image = function(element)
        return element.caption
      end,
      Link = contentHandler,
      Note = contentHandler,
      Quoted = contentHandler,
      SmallCaps = contentHandler,
      Span = contentHandler,
      Strikeout = contentHandler,
      Strong = contentHandler,
      Subscript = contentHandler,
      Superscript = contentHandler,
      Underline = contentHandler,
  
      -- default simple behavior
      Str = function(s)
        return { s.text }
      end,
      Space = function() return { " " } end,
      LineBreak = function() return { "\n" } end,
      SoftBreak = function() return { "\n" } end,
      Inlines = function(inlines)
        return inlines
      end,
      Blocks = function(blocks)
        return blocks
      end,
      RawInline = function(inline)
        local tbl, t = _quarto.ast.resolve_custom_data(inline)
        if tbl == nil then 
          return {}
        end
        local handler = _quarto.ast.resolve_handler(t)
        if handler == nil then
          return {}
        end
        local result = pandoc.List({})
        for _, v in ipairs(handler.inner_content(tbl)) do
          result:extend(v)
        end
        return result
      end
    }
  
    local function handleBottomUpResult(v)
      if type(v) == "string" then
        table.insert(resultingStrs, v)
      elseif type(v) == "userdata" then
        bottomUp(v)
      elseif tisarray(v) then
        for _, inner in ipairs(v) do
          bottomUp(v)
        end
      end
    end
    local bottomUp
  
    bottomUp = function(node)
      if type(node) == "string" then
        table.insert(resultingStrs, node)
        return nil
      end
      local t
      if type(node) == "userdata" then
        local tbl
        tbl, t = _quarto.ast.resolve_custom_data(node)
        if tbl ~= nil then 
          local astHandler = _quarto.ast.resolve_handler(t)
          if astHandler == nil then
            error("Internal error: no handler for " .. t)
            crash_with_stack_trace()
          end
          local nodeHandler = astHandler and handler[astHandler.ast_name] and handler[astHandler.ast_name].handle
          if nodeHandler == nil then
            local inner = astHandler.inner_content(tbl)
            for _, v in pairs(inner) do
              bottomUp(v)
            end
          else
            handleBottomUpResult(nodeHandler(tbl, bottomUp, node))
          end
        else
          local nodeHandler
          t = node.t or pandoc.utils.type(node)
          nodeHandler = handler[t] and handler[t].handle
          if nodeHandler == nil then 
            -- no handler, just walk the internals in some default order
            if bottomUpWalkers[t] then
              for _, v in ipairs(bottomUpWalkers[t](node)) do
                bottomUp(v)
              end
            else
              for _, v in pairs(node) do
                bottomUp(v)
              end
            end
          else
            handleBottomUpResult(nodeHandler(node, bottomUp))
          end
        end
      else
        -- allow
        t = type(node)
        local nodeHandler = handler[t]
        if nodeHandler ~= nil then
          handleBottomUpResult(nodeHandler(node, bottomUp))
        end
        if tisarray(node) then
          for _, v in ipairs(node) do
            bottomUp(v)
          end
        end
        -- do nothing if no handler for builtin type        
      end
    
      return nil
    end
  
    local wrappedFilter = {
      Pandoc = function(doc)
        local strs
        if handler.Writer then
          strs = handler.Writer.handle(doc)
        else
          bottomUp(doc)
          strs = table.concat(resultingStrs, "")
        end
        return pandoc.Pandoc(pandoc.Blocks(pandoc.RawBlock("markdown", strs .. "\n")))
      end
    }
    return wrappedFilter
  end))
end
-- meta.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- read and replace the authors field
-- without reshaped data that has been 
-- restructured into the standard author
-- format
local kAuthorInput =  'authors'

-- we ensure that if 'institute' is specified, we normalize it into
-- and array in this value, which this can safely read and process
local kInstituteInput = 'institutes'

-- By default, simply replace the input structure with the 
-- normalized versions of the output
local kAuthorOutput = kAuthorInput

-- Where we'll write the normalized list of affiliations
local kAffiliationOutput = "affiliations"

-- Where we'll write the normalized list of funding
local kFundingOutput = "funding"

-- Where we'll write the 'by-author' list of authors which
-- includes expanded affiliation information inline with the author
local kByAuthor = "by-author"

-- Where we'll write the 'by-affiliation' list of affiliations which
-- includes expanded author information inline with each affiliation
local kByAffiliation = "by-affiliation"
local kAuthors = "authors"

-- Funding placeholders
-- Normalized into:
-- funding:
--   - id: string (optional)
--     statement: string (optional)
--     open-access: string (optional)
--     source: 
--       - { text } | { institution }
--     investigator:
--       - { text } | { name } | { insitution }
--     recipient:
--       - { text } | { name } | { insitution }
local kFunding = "funding"
local kAwardId = 'id'
local kSource = "source"
local kStatement = "statement"
local kOpenAccess = "open-access"
local kRecipient = "recipient"
local kInvestigator = "investigator"

-- Properties that may appear on an individual author
local kId = 'id'
local kName = 'name'
local kUrl = 'url'
local kEmail = 'email'
local kFax = 'fax'
local kPhone = 'phone'
local kOrcid = 'orcid'
local kNote = 'note'
local kAcknowledgements = 'acknowledgements'
local kAffiliations = 'affiliations'
local kAffiliation = 'affiliation'
local kRef = 'ref'

-- attributes hold a list of strings which
-- represent true characteristics of the author
-- (for example, that they are the corresponding author)
-- the presence of a value means that it is true, the
-- absence of a value means that it is false
--
-- users can either write
-- attributes: [correspoding, is-equal-contributor]
-- or if attributes with these names are present (and truthy) 
-- on the author they will be collected into attributes.
-- For example-
--   author:
--     name: John Hamm
--     corresponding: true
--     is-equal-contributor: true
local kAttributes = 'attributes'

-- flag values for attributes (attributes is a list of 
-- flag names)
local kCorresponding = 'corresponding'
local kEqualContributor = 'equal-contributor'
local kDeceased = 'deceased'

-- metadata holds options that appear in the author key
-- that are not common to our author schema. we would like
-- to generally discourage this type of data since 
-- it will be difficult to reliably share across templates and
-- author representations, so we bucketize it here to 
-- suggest to users that this is 'other' data 
local kMetadata = 'metadata'

-- a name which will be structured into a name object that
-- look like:
-- name:
--   family:
--   given:
--   literal:
-- We can accept a literal string (which we parse to get the family and given)
-- or a structured object that declares all or some of the options directly
local kGivenName = 'given'
local kFamilyName = 'family'
local kLiteralName = 'literal'
local kDroppingParticle = 'dropping-particle'
local kNonDroppingParticle = 'non-dropping-particle'
local kNameFields = { kGivenName, kFamilyName, kLiteralName}

-- an affiliation which will be structured into a standalone
local kAffilName = 'name'
local kDepartment = 'department'
local kAddress = 'address'
local kCity = 'city'
local kRegion = 'region'
local kState = 'state'
local kCountry = 'country'
local kPostalCode = 'postal-code'
local kISNI = "isni"
local kRinggold = "ringgold"
local kROR = "ror"

-- labels contains the suggested labels for the various elements which 
-- are localized and should correctly deal with plurals, etc...
local kLabels = 'labels'
local kAuthorLbl = 'authors'
local kAffiliationLbl = 'affiliations'
local kPublishedLbl = 'published'
local kModifiedLbl = 'modified'
local kDoiLbl = 'doi'
local kDescriptionLbl = 'description'
local kAbstractLbl = 'abstract'

-- affiliation fields that might be parsed into other fields
-- (e.g. if we see affiliation-url with author, we make that affiliation/url)
local kAffiliationUrl = 'affiliation-url'

-- Titles are the values that we will accept in metadata to override the
-- default value for the above labels (e.g. abstract-title will provide the label)
-- for the abstract
local kAuthorTitle = 'author-title'
local kAffiliationTitle = 'affiliation-title'
local kAbstractTitle = 'abstract-title'
local kDescriptionTitle = 'description-title'
local kPublishedTitle = 'published-title'
local kModifiedTitle = 'modified-title'
local kDoiTitle = 'doi-title'

-- Deal with bibliography configuration as well
local kBiblioConfig = 'biblio-config'

-- The field types for an author (maps the field in an author table)
-- to the way the field should be processed
local kAuthorNameFields = { kName }
local kAuthorSimpleFields = { kId, kUrl, kEmail, kFax, kPhone, kOrcid, kAcknowledgements }
local kAuthorAttributeFields = { kCorresponding, kEqualContributor, kDeceased }
local kAuthorAffiliationFields = { kAffiliation, kAffiliations }

-- Fields for affiliations (either inline in authors or 
-- separately in a affiliations key)
local kAffiliationFields = { kId, kAffilName, kDepartment, kAddress, kCity, kRegion, kCountry, kPostalCode, kUrl, kISNI, kRinggold, kROR }

-- These affiliation fields will be mapped into 'region' 
-- (so users may also write 'state')
local kAffiliationAliasedFields = {
  [kState]=kRegion,
  [kAffiliationUrl]=kUrl
}

-- This field will be included with 'by-author' and 'by-affiliation' and provides
-- a simple incremental counter that can be used for things like note numbers
local kNumber = "number"
local kLetter = "letter"

function processAuthorMeta(meta)
  -- prevents the front matter for markdown from containing
  -- all the rendered author information that we generate
  if _quarto.format.isMarkdownOutput() then
    meta[kAuthors] = nil
    return meta
  end

  -- prefer to render 'authors' if it is available
  local authorsRaw = meta[kAuthorInput]
  if meta[kAuthors] then
    authorsRaw = meta[kAuthors]
  end

  -- authors should be a table of tables (e.g. it should be an array of inlines or tables)
  -- if it isn't, transform it into one
  if type(authorsRaw) == "table" then
    if (type(authorsRaw[1]) ~= "table") then
      authorsRaw = {authorsRaw}
    end
  end


  -- the normalized authors
  local authors = {}

  -- the normalized affilations
  local affiliations = {}

  if authorsRaw then
    for i,v in ipairs(authorsRaw) do

      local authorAndAffiliations = processAuthor(v)

      -- initialize the author
      local author = authorAndAffiliations.author
      local authorAffils = authorAndAffiliations.affiliations

      -- assign an id to this author if one isn't defined
      local authorNumber = #authors + 1
      if author[kId] == nil then
        author[kId] = authorNumber
      end

      -- go through the affilations and add any to the list
      -- assigning an id if needed
      if authorAffils ~= nil then
        for i,v in ipairs(authorAffils) do
          local affiliation = maybeAddAffiliation(v, affiliations)
          setAffiliation(author, { ref=affiliation[kId] })
        end
      end

      -- add this author to the list of authors
      authors[authorNumber] = author
    end
  end

  -- Add any affiliations that are explicitly specified
  local affiliationsRaw = meta[kAffiliations]
  if affiliationsRaw then
    local explicitAffils = processAffiliation(nil, affiliationsRaw)
    if explicitAffils then
      for i,affiliation in ipairs(explicitAffils) do
        local addedAffiliation = maybeAddAffiliation(affiliation, affiliations)

        -- for any authors that are using this affiliation, fix up their reference
        if affiliation[kId] ~= addedAffiliation[kId] then
          remapAuthorAffiliations(affiliation[kId], addedAffiliation[kId], authors)
        end
      end
    end
  end

  -- process 'institute', which is used by revealjs and beamer
  -- because they bear no direct relation to the authors
  -- we will just use their position to attach them
  local instituteRaw = meta[kInstituteInput]
  if instituteRaw then
    for i,institute in ipairs(instituteRaw) do
      -- add the affiliation
      local affiliation = processAffilationObj({ name=institute })
      local addedAffiliation = maybeAddAffiliation(affiliation, affiliations)

      -- note the reference on the author
      -- if there aren't enough authors, attach the affiliations to the
      -- last author
      local author = authors[#authors]
      if i <= #authors then
        author = authors[i]
      end
      if author then
        setAffiliation(author, { ref=addedAffiliation[kId] })
      end
    end
  end

  -- validate that every author affiliation has a corresponding 
  -- affiliation defined in the affiliations key
  validateRefs(authors, affiliations)

  -- number the authors and affiliations
  for i,affil in ipairs(affiliations) do
    affil[kNumber] = i
    affil[kLetter] = letter(i)
  end
  for i,auth in ipairs(authors) do
    auth[kNumber] = i
    auth[kLetter] = letter(i)
  end

  -- Write the normalized data back to metadata
  if #authors ~= 0 then
    meta[kAuthorOutput] = authors
  end

  if #affiliations ~= 0 then
    meta[kAffiliationOutput] = affiliations
  end

  -- Write the de-normalized versions back to metadata
  if #authors ~= 0 then
    meta[kByAuthor] = byAuthors(authors, affiliations)
  end

  if #affiliations ~= 0 then
    meta[kByAffiliation] = byAffiliations(authors, affiliations)
  end

  -- the normalized funding
  local funding = {}

  -- process the 'funding' key
  local fundingRaw = meta[kFunding]
  if fundingRaw then
    -- ensure that this is table
    if pandoc.utils.type(fundingRaw) ~= "List" then
      fundingRaw = pandoc.List({fundingRaw})
    end
    
  
    for i,fundingAward in ipairs(fundingRaw) do 
      local normalizedAward = processFundingAward(fundingAward, authors, affiliations)
      if normalizedAward then
        funding[i] = normalizedAward
      end 
    end
  end
  
  -- write the normalized funding to output
  if #funding ~= 0 then
    meta[kFundingOutput] = funding
  end 

  -- Provide localized or user specified strings for title block elements
  meta = computeLabels(authors, affiliations, meta)

  -- Provide biblio-config if it isn't specified
  if meta[kBiblioConfig] == nil and not _quarto.format.isAstOutput() then
    meta[kBiblioConfig] = true
  end

  return meta
end

-- Add an affiliation to the list of affiliations if needed
-- and return either the exist affiliation, or the newly
-- added affiliation with a proper id
function maybeAddAffiliation(affiliation, affiliations)
  local existingAff = findMatchingAffililation(affiliation, affiliations)
  if existingAff == nil then
    local affiliationNumber = #affiliations + 1
    local affiliationId = 'aff-' .. affiliationNumber
    if affiliation[kId] == nil then
      affiliation[kId] = { pandoc.Str(affiliationId) }
    end
    affiliations[affiliationNumber] = affiliation
    return affiliation
  else
    return existingAff
  end
end

function validateRefs(authors, affiliations)
  -- iterate through affiliations and ensure that anything
  -- referenced by an author has a peer affiliation

  -- get the list of affiliation ids
  local affilIds = {}
  if affiliations then
    for i,affiliation in ipairs(affiliations) do
      affilIds[#affilIds + 1] = affiliation[kId]
    end
  end

  -- go through each author and their affiliations and 
  -- ensure that they are in the list
  for i,author in ipairs(authors) do
    if author[kAffiliations] then
      for i,affiliation in ipairs(author[kAffiliations]) do
        if not tcontains(affilIds, affiliation[kRef]) then
          error("Undefined affiliation '" .. pandoc.utils.stringify(affiliation[kRef]) .. "' for author '" .. pandoc.utils.stringify(author[kName][kLiteralName]) .. "'.")
          os.exit(1)
        end
      end
    end
  end
end

-- Processes an individual author into a normalized author
-- and normalized set of affilations
function processAuthor(value)
  -- initialize the author
  local author = pandoc.MetaMap({})
  author[kMetadata] = pandoc.MetaMap({})

  -- initialize their affilations
  local authorAffiliations = {}
  local affiliationUrl = nil

  if pandoc.utils.type(value) == 'Inlines' then
    -- The value is simply an array, treat them as the author name
    author.name = toName(value);
  else
    -- Process the field into the proper place in the author
    -- structure
    for authorKey, authorValue in pairs(value) do
      if tcontains(kAuthorNameFields, authorKey) then
        -- process any names
        author[authorKey] = toName(authorValue)
      elseif tcontains(kAuthorSimpleFields, authorKey) then
        -- process simple fields
        author[authorKey] = authorValue
      elseif tcontains(kAuthorAttributeFields, authorKey) then
        -- process a field into attributes (a field that appears)
        -- directly under the author
        if authorValue then
          setAttribute(author, pandoc.Str(authorKey))
        end
      elseif authorKey == kAttributes then
        -- process an explicit attributes key
        processAttributes(author, authorValue)
      elseif authorKey == kNote then
        processAuthorNote(author, authorValue)
      elseif tcontains(kAuthorAffiliationFields, authorKey) then
        -- process affiliations that are specified in the author
        authorAffiliations = processAffiliation(author, authorValue)
      elseif authorKey == kAffiliationUrl then
        affiliationUrl = authorValue
      else
        -- since we don't recognize this value, place it under
        -- metadata to make it accessible to consumers of this 
        -- data structure
        setMetadata(author, authorKey, authorValue)
      end
    end
  end

  -- If there is an affiliation url, forward that along
  if authorAffiliations and affiliationUrl then
    authorAffiliations[1][kUrl] = affiliationUrl
  end

  return {
    author=author,
    affiliations=authorAffiliations
  }
end

-- Processes an affiatiation into a normalized
-- affilation
function processAffiliation(author, affiliation)
  local affiliations = {}
  local pandocType = pandoc.utils.type(affiliation)
  if pandocType == 'Inlines' then
    -- The affiliations is simple a set of inlines,  use
    affiliations[#affiliations + 1] = processAffilationObj({ name=affiliation })
  elseif pandocType == 'List' then
    for i, v in ipairs(affiliation) do
      if pandoc.utils.type(v) == 'Inlines' then
        -- This item is just a set inlines, use that as the name
        affiliations[#affiliations + 1] = processAffilationObj({ name=v })
      else
        local keys = tkeys(v)
        if keys and #keys == 1 and keys[1] == kRef then
          -- See if this is just an item with a 'ref', and if it is, just pass
          -- it through on the author
          if author then
            setAffiliation(author, v)
          end
        else
          -- This is a more complex affilation, process it
          affiliations[#affiliations + 1] = processAffilationObj(v)
        end
      end
    end
  elseif pandocType == 'table' then
    -- This is a more complex affilation, process it
    affiliations[#affiliations + 1] = processAffilationObj(affiliation)
  end



  return affiliations
end

-- Normalizes an indivudal funding entry
function processFundingAward(fundingAward, authors, affiliations)
  if pandoc.utils.type(fundingAward) == 'table' then
    
    -- this is a table of properties, process them
    local result = {}

    -- process the simple values
    for i, key in ipairs({ kAwardId, kStatement, kOpenAccess }) do
      local valueRaw = fundingAward[key]
      if valueRaw ~= nil then
        result[key] = valueRaw
      end
    end

    -- Process the funding source
    local sourceRaw = fundingAward[kSource]
    if sourceRaw ~= nil then
      result[kSource] = processSources(sourceRaw)     
    end

    -- Process recipients
    local recipientRaw = fundingAward[kRecipient]
    if recipientRaw ~= nil then
      result[kRecipient] = processNameOrInstitution(kRecipient, recipientRaw, authors, affiliations)
    end

    local investigatorRaw = fundingAward[kInvestigator]
    if investigatorRaw ~= nil then
      result[kInvestigator] = processNameOrInstitution(kInvestigator, investigatorRaw, authors, affiliations)
    end

    return result
  else
    
    -- this is a simple string / inlines, just 
    -- use it as the source
    return {
      [kStatement] = fundingAward
    }
  end
end

function processNameOrInstitution(keyName, values, authors, affiliations) 
  if values ~= nil then
    local pandocType = pandoc.utils.type(values)
    if pandocType == "List" then
      local results = pandoc.List()
      for i, value in ipairs(values) do
        results:insert(processNameOrInstitutionObj(keyName, value, authors, affiliations))
      end
      return results
    else
      return { processNameOrInstitutionObj(values, values, authors, affiliations) }
    end
  else 
    return {}
  end
end


function processSources(sourceRaw)
  local pandocType = pandoc.utils.type(sourceRaw)
  if pandocType == 'Inlines' then
    return {{ text = sourceRaw }}
  else
    local result = pandoc.List()
    for i, value in ipairs(sourceRaw) do
      if pandoc.utils.type(value) == 'Inlines' then
        result:insert({ text = value})
      else
        result:insert(value)
      end
    end
    return result
  end
end

-- Normalizes a value that could be either a plain old markdown string,
-- a name, or an affiliation
function processNameOrInstitutionObj(keyName, valueRaw, authors, affiliations)
  if (pandoc.utils.type(valueRaw) == 'Inlines') then
    return { text = valueRaw }
  else
    if valueRaw.name ~= nil then
      return { name = toName(valueRaw.name) }
    elseif valueRaw.institution ~= nil then
      return { institition = processAffilationObj(valueRaw.institution) }
    elseif valueRaw.ref ~= nil then
      local refStr = pandoc.utils.stringify(valueRaw.ref)

      -- discover the reference (could be author or affiliation)
      local affiliation = findAffiliation({{ text = refStr }}, affiliations)
      if affiliation then
        return { institition = affiliation }
      else
        local author = findAuthor(refStr, authors)
        if author then
          return { [kName] = author[kName] }
        else
          error("Invalid funding ref " .. refStr)
          os.exit(1)
        end
      end
    else
      error("Invalid value for " .. keyName)
      os.exit(1)
    end
  end
end

-- Normalizes an affilation object into the properly
-- structured form
function processAffilationObj(affiliation)
  local affiliationNormalized = {}
  affiliationNormalized[kMetadata] = {}


  for affilKey, affilVal in pairs(affiliation) do
    if (tcontains(tkeys(kAffiliationAliasedFields), affilKey)) then
      affiliationNormalized[kAffiliationAliasedFields[affilKey]] = affilVal
    elseif tcontains(kAffiliationFields, affilKey) then
      affiliationNormalized[affilKey] = affilVal
    else
      affiliationNormalized[kMetadata][affilKey] = affilVal
    end
  end

  return affiliationNormalized;
end

-- Finds a matching affiliation by looking through a list
-- of affiliations (ignoring the id)
function findMatchingAffililation(affiliation, affiliations)
  for i, existingAffiliation in ipairs(affiliations) do

    -- an affiliation matches if the fields other than id
    -- are identical
    local matches = true
    for j, field in ipairs(kAffiliationFields) do
      if field ~= kId and matches then
        matches = affiliation[field] == existingAffiliation[field]
      end
    end

    -- This affiliation matches, return it
    if matches then
      return existingAffiliation
    end
  end
  return nil
end

-- Replaces an affiliation reference with a different id
-- (for example, if a reference to an affiliation is collapsed into a single
-- entry with a single id)
function remapAuthorAffiliations(fromId, toId, authors)
  for i, author in ipairs(authors) do
    for j, affiliation in ipairs(author[kAffiliations]) do
      local existingRefId = affiliation[kRef]
      if existingRefId == fromId then
        affiliation[kRef] = toId
      end
     end
  end
end

-- Process attributes onto an author
-- attributes may be a simple string, a list of strings
-- or a dictionary
function processAttributes(author, attributes)
  if tisarray(attributes) then
    -- process attributes as an array of values
    for i,v in ipairs(attributes) do
      if v then
        if v.t == "Str" then
          setAttribute(author, v)
        else
          for j, attr in ipairs(v) do
            setAttribute(author, attr)
          end
        end
      end
    end
  else
    -- process attributes as a dictionary
    for k,v in pairs(attributes) do
      if v then
        setAttribute(author, pandoc.Str(k))
      end
    end
  end
end

-- Process an author note (including numbering it)
local noteNumber = 1
function processAuthorNote(author, note)
  author[kNote] = {
    number=noteNumber,
    text=note
  }
  noteNumber = noteNumber + 1
end

-- Sets a metadata value, initializing the table if
-- it not yet defined
function setMetadata(author, key, value)
  author[kMetadata][key] = value
end

-- Sets an attribute, initializeing the table if
-- is not yet defined
function setAttribute(author, attribute)
  if not author[kAttributes] then
    author[kAttributes] = pandoc.MetaMap({})
  end

  local attrStr = pandoc.utils.stringify(attribute)
  -- Don't duplicate attributes
  if not author[kAttributes][attrStr] then
    author[kAttributes][attrStr] = pandoc.Str('true')
  end
end

function setAffiliation(author, affiliation)
  if not author[kAffiliations] then
    author[kAffiliations] = {}
  end
  author[kAffiliations][#author[kAffiliations] + 1] = affiliation
end


-- Converts name elements into a structured name
function toName(nameParts)
  if not tisarray(nameParts) then
    -- If the name is a table (e.g. already a complex object)
    -- just pick out the allowed fields and forward
    local name = {}
    for i,v in ipairs(kNameFields) do
      if nameParts[v] ~= nil then
        name[v] = nameParts[v]
      end
    end

    return normalizeName(name)
  else
    if #nameParts == 0 then
      return {}
    else
      return normalizeName({[kLiteralName] = nameParts})
    end
  end
end

-- normalizes a name value by parsing it into
-- family and given names
function normalizeName(name)
  -- no literal name, create one
  if name[kLiteralName] == nil then
    if name[kFamilyName] and name[kGivenName] then
      name[kLiteralName] = {}
      tappend(name[kLiteralName], name[kGivenName])
      tappend(name[kLiteralName], {pandoc.Space()})
      tappend(name[kLiteralName], name[kFamilyName])
    end
  end

  -- no family or given name, parse the literal and create one
  if name[kFamilyName] == nil or name[kGivenName] == nil then
    if name[kLiteralName] then
      local parsedName = bibtexParseName(name)
      if type(parsedName) == 'table' then
        if parsedName.given ~= nil then
          name[kGivenName] = {pandoc.Str(parsedName.given)}
        end
        if parsedName.family ~= nil then
          name[kFamilyName] = {pandoc.Str(parsedName.family)}
        end
        if name[kDroppingParticle] ~= nil then
          name[kDroppingParticle] = parsedName[kDroppingParticle]
        end
        if name[kNonDroppingParticle] ~= nil then
          name[kNonDroppingParticle] = parsedName[kNonDroppingParticle]
        end
      else
        if #name[kLiteralName] > 1 then
          -- bibtex parsing failed, just split on space
          name[kGivenName] = name[kLiteralName][1]
          name[kFamilyName] = trimspace(tslice(name[kLiteralName], 2))
        elseif name[kLiteralName] then
          -- what is this thing, just make it family name
          name[kFamilyName] = name[kLiteralName]
        end
      end
    end
  end
  return name
end

local kBibtexNameTemplate = [[
@misc{x,
  author = {%s}
}
]]

--- Returns a CSLJSON-like name table. BibTeX knows how to parse names,
--- so we leverage that.
function bibtexParseName(nameRaw)
  local bibtex = kBibtexNameTemplate:format(pandoc.utils.stringify(nameRaw))
  local references = pandoc.read(bibtex, 'bibtex').meta.references
  if references then
    local reference = references[1] --[[@as table<string,any>]]
    if reference then
      local authors = reference.author
      if authors then
        local name = authors[1]
        if type(name) ~= 'table' then
          return nameRaw
        else
          -- most dropping particles are really non-dropping
          if name['dropping-particle'] and not name['non-dropping-particle'] then
            name['non-dropping-particle'] = name['dropping-particle']
            name['dropping-particle'] = nil
          end
          return name
        end
      else
        return nameRaw
      end
    else
      return nameRaw
    end
  else
    return nameRaw
  end
end

function byAuthors(authors, affiliations)
  local denormalizedAuthors = deepCopy(authors)

  if denormalizedAuthors then
    for i, author in ipairs(denormalizedAuthors) do
      denormalizedAuthors[kNumber] = i
      local authorAffiliations = author[kAffiliations]
      if authorAffiliations then
        for j, affilRef in ipairs(authorAffiliations) do
          local id = affilRef[kRef]
          author[kAffiliations][j] = findAffiliation(id, affiliations)
        end
      end
    end
  end
  return denormalizedAuthors
end

function byAffiliations(authors, affiliations)
  local denormalizedAffiliations = deepCopy(affiliations)
  for i, affiliation in ipairs(denormalizedAffiliations) do
    local affilAuthor = findAffiliationAuthors(affiliation[kId], authors)
    if affilAuthor then
      affiliation[kAuthors] = affilAuthor
    end
  end
  return denormalizedAffiliations
end

-- Finds a matching affiliation by id
function findAffiliation(id, affiliations)
  for i, affiliation in ipairs(affiliations) do
    if affiliation[kId][1].text == id[1].text then
      return affiliation
    end
  end
  return nil
end

-- Finds a matching author by affiliation id
function findAffiliationAuthors(id, authors)
  local matchingAuthors = {}
  for i, author in ipairs(authors) do
    local authorAffils = author[kAffiliations]
    if authorAffils then
      for j, authorAffil in ipairs(authorAffils) do
        if authorAffil[kRef][1].text == id[1].text then
          matchingAuthors[#matchingAuthors + 1] = author
        end
      end
    end
  end
  return matchingAuthors
end

-- Finds a matching author by author id
function findAuthor(id, authors)
  for i, author in ipairs(authors) do
    if pandoc.utils.stringify(author[kId]) == id then
      return author
    end
  end
  return nil
end


-- Resolve labels for elements into metadata
function computeLabels(authors, affiliations, meta)
  local language = param("language", nil);

  if not _quarto.format.isAstOutput() then
    meta[kLabels] = {
      [kAuthorLbl] = {pandoc.Str("Authors")},
      [kAffiliationLbl] = {pandoc.Str("Affiliations")}
    }
    if #authors == 1 then
      meta[kLabels][kAuthorLbl] = {pandoc.Str(language["title-block-author-single"])}
    else
      meta[kLabels][kAuthorLbl] = {pandoc.Str(language["title-block-author-plural"])}
    end
    if meta[kAuthorTitle] then
      meta[kLabels][kAuthors] = meta[kAuthorTitle]
    end

    if #affiliations == 1 then
      meta[kLabels][kAffiliationLbl] = {pandoc.Str(language["title-block-affiliation-single"])}
    else
      meta[kLabels][kAffiliationLbl] = {pandoc.Str(language["title-block-affiliation-plural"])}
    end
    if meta[kAffiliationTitle] then
      meta[kLabels][kAffiliationLbl] = meta[kAffiliationTitle]
    end

    meta[kLabels][kPublishedLbl] = {pandoc.Str(language["title-block-published"])}
    if meta[kPublishedTitle] then
      meta[kLabels][kPublishedLbl] = meta[kPublishedTitle]
    end

    meta[kLabels][kModifiedLbl] = {pandoc.Str(language["title-block-modified"])}
    if meta[kModifiedTitle] then
      meta[kLabels][kModifiedLbl] = meta[kModifiedTitle]
    end

    meta[kLabels][kDoiLbl] = {pandoc.Str("Doi")}
    if meta[kDoiTitle] then
      meta[kLabels][kDoiLbl] = meta[kDoiTitle]
    end

    meta[kLabels][kAbstractLbl] = {pandoc.Str(language["section-title-abstract"])}
    if meta[kAbstractTitle] then
      meta[kLabels][kAbstractLbl] = meta[kAbstractTitle]
    end

    meta[kLabels][kDescriptionLbl] = {pandoc.Str(language["listing-page-field-description"])}
    if meta[kDescriptionTitle] then
      meta[kLabels][kDescriptionLbl] = meta[kDescriptionTitle]
    end
  end

  return meta
end

-- Get a letter for a number 
function letter(number)
  number = number%26
  return string.char(96 + number)
end

-- Remove Spaces from the ends of tables
function trimspace(tbl)
  if #tbl > 0 then
    if tbl[1].t == 'Space' then
      tbl = tslice(tbl, 2)
    end
  end

  if #tbl > 0 then
    if tbl[#tbl].t == 'Space' then
      tbl = tslice(tbl, #tbl -1)
    end
  end
  return tbl
end

-- Deep Copy a table
function deepCopy(original)
	local copy = {}
	for k, v in pairs(original) do
		if type(v) == "table" then 
			v = deepCopy(v)
		end
		copy[k] = v
	end
	return copy
end
---@diagnostic disable: undefined-field
--[[

 base64 -- v1.5.3 public domain Lua base64 encoder/decoder
 no warranty implied; use at your own risk

 Needs bit32.extract function. If not present it's implemented using BitOp
 or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
 implementation inspired by Rici Lake's post:
   http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html

 author: Ilya Kolbin (iskolbin@gmail.com)
 url: github.com/iskolbin/lbase64

 COMPATIBILITY

 Lua 5.1+, LuaJIT

 LICENSE

 See end of file for license information.

--]]


local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode
if not extract then
	if _G.bit then -- LuaJIT
		local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
		extract = function( v, from, width )
			return band( shr( v, from ), shl( 1, width ) - 1 )
		end
	elseif _G._VERSION == "Lua 5.1" then
		extract = function( v, from, width )
			local w = 0
			local flag = 2^from
			for i = 0, width-1 do
				local flag2 = flag + flag
				if v % flag2 >= flag then
					w = w + 2^i
				end
				flag = flag2
			end
			return w
		end
	else -- Lua 5.3+
		extract = load[[return function( v, from, width )
			return ( v >> from ) & ((1 << width) - 1)
		end]]()
	end
end


function base64_makeencoder( s62, s63, spad )
	local encoder = {}
	for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
		'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
		'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
		'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
		'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
		encoder[b64code] = char:byte()
	end
	return encoder
end

function base64_makedecoder( s62, s63, spad )
	local decoder = {}
	for b64code, charcode in pairs( base64_makeencoder( s62, s63, spad )) do
		decoder[charcode] = b64code
	end
	return decoder
end

local DEFAULT_ENCODER = base64_makeencoder()
local DEFAULT_DECODER = base64_makedecoder()

local char, concat = string.char, table.concat

function base64_encode( str, encoder, usecaching )
	encoder = encoder or DEFAULT_ENCODER
	local t, k, n = {}, 1, #str
	local lastn = n % 3
	local cache = {}
	for i = 1, n-lastn, 3 do
		local a, b, c = str:byte( i, i+2 )
		local v = a*0x10000 + b*0x100 + c
		local s
		if usecaching then
			s = cache[v]
			if not s then
				s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
				cache[v] = s
			end
		else
			s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
		end
		t[k] = s
		k = k + 1
	end
	if lastn == 2 then
		local a, b = str:byte( n-1, n )
		local v = a*0x10000 + b*0x100
		t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
	elseif lastn == 1 then
		local v = str:byte( n )*0x10000
		t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
	end
	return concat( t )
end

function base64_decode( b64, decoder, usecaching )
	decoder = decoder or DEFAULT_DECODER
	local pattern = '[^%w%+%/%=]'
	if decoder then
		local s62, s63
		for charcode, b64code in pairs( decoder ) do
			if b64code == 62 then s62 = charcode
			elseif b64code == 63 then s63 = charcode
			end
		end
		pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
	end
	b64 = b64:gsub( pattern, '' )
	local cache = usecaching and {}
	local t, k = {}, 1
	local n = #b64
	local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
	for i = 1, padding > 0 and n-4 or n, 4 do
		local a, b, c, d = b64:byte( i, i+3 )
		local s
		if usecaching then
			local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
			s = cache[v0]
			if not s then
				local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
				s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
				cache[v0] = s
			end
		else
			local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
			s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
		end
		t[k] = s
		k = k + 1
	end
	if padding == 1 then
		local a, b, c = b64:byte( n-3, n-1 )
		local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
		t[k] = char( extract(v,16,8), extract(v,8,8))
	elseif padding == 2 then
		local a, b = b64:byte( n-3, n-2 )
		local v = decoder[a]*0x40000 + decoder[b]*0x1000
		t[k] = char( extract(v,16,8))
	end
	return concat( t )
end

--[[
------------------------------------------------------------------------------
This software is available under 2 licenses -- choose whichever you prefer.
------------------------------------------------------------------------------
ALTERNATIVE A - MIT License
Copyright (c) 2018 Ilya Kolbin
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
------------------------------------------------------------------------------
ALTERNATIVE B - Public Domain (www.unlicense.org)
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
software, either in source code form or as a compiled binary, for any purpose,
commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and to
the detriment of our heirs and successors. We intend this dedication to be an
overt act of relinquishment in perpetuity of all present and future rights to
this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
------------------------------------------------------------------------------
--]]
-- citation.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- read and replace the citation field
-- with reshaped data that has been 
-- restructured into the standard has
-- format

local kCitation = "citation"
local kContainerId = "container-id"
local kArticleId = "article-id"
local kPage = "page"
local kPageFirst = "page-first"
local kPageLast = "page-last"


local function processTypedId(el) 
  if pandoc.utils.type(el) == "Inlines" then
    return { value = el }
  else
    return el    
  end
end

local function normalizeTypedId(els)
  if pandoc.utils.type(els) == "List" then
    -- this is a list of ids
    local normalizedEls = {}
    for i,v in ipairs(els) do        
      local normalized = processTypedId(v)
      tappend(normalizedEls, {normalized})
    end
    return normalizedEls
  elseif pandoc.utils.type(els) == "Inlines" then
    -- this is a simple id (a string)
    return { processTypedId(els )}
  else
    -- this is a single id, but is already a typed id
    return { processTypedId(els)}
  end
end

function processCitationMeta(meta)
  if meta then
    local citationMeta = meta[kCitation]
    if citationMeta then

      local containerIds = citationMeta[kContainerId]
      if containerIds ~= nil then
        meta[kCitation][kContainerId] = normalizeTypedId(containerIds)
      end

      local articleIds = citationMeta[kArticleId]
      if articleIds ~= nil then
        meta[kCitation][kArticleId] = normalizeTypedId(articleIds)
      end

      if citationMeta[kPage] and citationMeta[kPageFirst] == nil and citationMeta[kPageLast] == nil then
        local pagerange = split(pandoc.utils.stringify(citationMeta[kPage]), '-')
        meta[kCitation][kPageFirst] = pandoc.Inlines(pagerange[1])
        if pagerange[2] then
          meta[kCitation][kPageLast] = pandoc.Inlines(pagerange[2])
        end
      end
    end
    return meta
  end
end

-- colors.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- These colors are used as background colors with an opacity of 0.75
kColorUnknown = "909090"
kColorNote = "0758E5"
kColorImportant = "CC1914"
kColorWarning = "EB9113"
kColorTip = "00A047"
kColorCaution = "FC5300"

-- these colors are used with no-opacity
kColorUnknownFrame = "acacac"
kColorNoteFrame = "4582ec"
kColorImportantFrame = "d9534f"
kColorWarningFrame = "f0ad4e"
kColorTipFrame = "02b875"
kColorCautionFrame = "fd7e14"

kBackgroundColorUnknown = "e6e6e6"
kBackgroundColorNote = "dae6fb"
kBackgroundColorImportant = "f7dddc"
kBackgroundColorWarning = "fcefdc"
kBackgroundColorTip = "ccf1e3"
kBackgroundColorCaution = "ffe5d0"

function latexXColor(color) 
  -- remove any hash at the front
  color = pandoc.utils.stringify(color)
  color = color:gsub("#","")

  local hexCount = 0
  for match in color:gmatch "%x%x" do
    hexCount = hexCount + 1
  end

  if hexCount == 3 then
    -- this is a hex color
    return "{HTML}{" .. color .. "}"
  else
    -- otherwise treat it as a named color
    -- and hope for the best
    return '{named}{' .. color .. '}' 
  end
end

-- converts a hex string to a RGB
function hextoRgb(hex)
  -- remove any leading #
  hex = hex:gsub("#","")

  -- convert to 
  return {
    red = tonumber("0x"..hex:sub(1,2)), 
    green = tonumber("0x"..hex:sub(3,4)), 
    blue = tonumber("0x"..hex:sub(5,6))
  }
end

-- debug.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- improved formatting for dumping tables
function tdump (tbl, indent, refs)
  if not refs then refs = {} end
  if not indent then indent = 0 end
  local address = string.format("%p", tbl)
  if refs[address] ~= nil then
    print(string.rep("  ", indent) .. "(circular reference to " .. address .. ")")
    return
  end

  if tbl.t and type(t) == "string" then
    print(string.rep("  ", indent) .. tbl.t)
  end
  local empty = true
  for k, v in pairs(tbl) do
    empty = false
    formatting = string.rep("  ", indent) .. k .. ": "
    v = asLua(v)
    if type(v) == "table" then
      print(formatting .. "table: " .. address)
      refs[address] = true
      tdump(v, indent+1, refs)
    elseif type(v) == 'boolean' then
      print(formatting .. tostring(v))
    elseif (v ~= nil) then 
      print(formatting .. tostring(v))
    else 
      print(formatting .. 'nil')
    end
  end
  if empty then
    print(string.rep("  ", indent) .. "<empty table>")
  end
end

function asLua(o)
  if type(o) ~= 'userdata' then
    return o
  end
  
  if rawequal(o, PANDOC_READER_OPTIONS) then
    return {
      abbreviations = o.abbreviations,
      columns = o.columns,
      default_image_extension = o.default_image_extension,
      extensions = o.extensions,
      indented_code_classes = o.indented_code_classes,
      standalone = o.standalone,
      strip_comments = o.strip_comments,
      tab_stop = o.tab_stop,
      track_changes = o.track_changes,
    }
  elseif rawequal(o, PANDOC_WRITER_OPTIONS) then
    return {
      cite_method = o.cite_method,
      columns = o.columns,
      dpi = o.dpi,
      email_obfuscation = o.email_obfuscation,
      epub_chapter_level = o.epub_chapter_level,
      epub_fonts = o.epub_fonts,
      epub_metadata = o.epub_metadata,
      epub_subdirectory = o.epub_subdirectory,
      extensions = o.extensions,
      highlight_style = o.highlight_style,
      html_math_method = o.html_math_method,
      html_q_tags = o.html_q_tags,
      identifier_prefix = o.identifier_prefix,
      incremental = o.incremental,
      listings = o.listings,
      number_offset = o.number_offset,
      number_sections = o.number_sections,
      prefer_ascii = o.prefer_ascii,
      reference_doc = o.reference_doc,
      reference_links = o.reference_links,
      reference_location = o.reference_location,
      section_divs = o.section_divs,
      setext_headers = o.setext_headers,
      slide_level = o.slide_level,
      tab_stop = o.tab_stop,
      table_of_contents = o.table_of_contents,
      template = o.template,
      toc_depth = o.toc_depth,
      top_level_division = o.top_level_division,
      variables = o.variables,
      wrap_text = o.wrap_text
    }
  end
  v = tostring(o)
  if string.find(v, "^pandoc CommonState") then
    return {
      input_files = o.input_files,
      output_file = o.output_file,
      log = o.log,
      request_headers = o.request_headers,
      resource_path = o.resource_path,
      source_url = o.source_url,
      user_data_dir = o.user_data_dir,
      trace = o.trace,
      verbosity = o.verbosity
    }
  elseif string.find(v, "^pandoc LogMessage") then
    return v
  end
  return o
end

-- dump an object to stdout
local function dump(o)
  o = asLua(o)
  if type(o) == 'table' then
    tdump(o)
  else
    print(tostring(o) .. "\n")
  end
end
-- debug.lua
-- Copyright (C) 2020-2022 Posit Software, PBC





function fail(message)
  local file = currentFile()
  if file then
    print("An error occurred while processing '" .. file .. "'")
  else
    print("An error occurred")
  end
  print(message)
  os.exit(1)
end


function currentFile() 
  
  if currentFileMetadataState ~= nil then
    -- if we're in a multifile contatenated render, return which file we're rendering
    local fileState = currentFileMetadataState()
    if fileState ~= nil and fileState.file ~= nil and fileState.file.bookItemFile ~= nil then
      return fileState.file.bookItemFile
    elseif fileState ~= nil and fileState.include_directory ~= nil then
      return fileState.include_directory
    else
      return nil
    end
  else
    -- if we're not in a concatenated scenario, file name doesn't really matter since the invocation is only
    -- targeting a single file
    return nil
  end
end
-- figures.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- constants for figure attributes
kFigAlign = "fig-align"
kFigEnv = "fig-env"
kFigAlt = "fig-alt"
kFigPos = "fig-pos"
kFigCap = "fig-cap"
kFigScap = "fig-scap"
kResizeWidth = "resize.width"
kResizeHeight = "resize.height"


function isFigAttribute(name)
  return string.find(name, "^fig%-")
end

function figAlignAttribute(el)
  local default = pandoc.utils.stringify(
    param(kFigAlign, pandoc.Str("default"))
  )
  local align = attribute(el, kFigAlign, default)
  if align == "default" then
    align = default
  end
  return validatedAlign(align)
end

-- is this an image containing a figure
function isFigureImage(el)
  return hasFigureRef(el) and #el.caption > 0
end

-- is this a Div containing a figure
function isFigureDiv(el)
  if el.t == "Div" and hasFigureRef(el) then
    return refCaptionFromDiv(el) ~= nil
  else
    return discoverLinkedFigureDiv(el) ~= nil
  end
end

function discoverFigure(el, captionRequired)
  if el.t ~= "Para" then
    return nil
  end
  if captionRequired == nil then
    captionRequired = true
  end
  if #el.content == 1 and el.content[1].t == "Image" then
    local image = el.content[1]
    if not captionRequired or #image.caption > 0 then
      return image
    else
      return nil
    end
  else
    return nil
  end
end

function discoverLinkedFigure(el, captionRequired)
  if el.t ~= "Para" then
    return nil
  end
  if #el.content == 1 then 
    if el.content[1].t == "Link" then
      local link = el.content[1]
      if #link.content == 1 and link.content[1].t == "Image" then
        local image = link.content[1]
        if not captionRequired or #image.caption > 0 then
          return image
        end
      end
    end
  end
  return nil
end

function createFigureDiv(paraEl, fig)
  
  -- create figure div
  local figureDiv = pandoc.Div({})
 
  -- transfer identifier
  figureDiv.attr.identifier = fig.attr.identifier
  fig.attr.identifier = ""
  
  -- provide anonymous identifier if necessary
  if figureDiv.attr.identifier == "" then
    figureDiv.attr.identifier = anonymousFigId()
  end
  
  -- transfer classes
  figureDiv.attr.classes = fig.attr.classes:clone()
  tclear(fig.attr.classes)
  
  -- transfer fig. attributes
  for k,v in pairs(fig.attr.attributes) do
    if isFigAttribute(k) then
      figureDiv.attr.attributes[k] = v
    end
  end
  local attribs = tkeys(fig.attr.attributes)
  for _,k in ipairs(attribs) do
    if isFigAttribute(k) then
      fig.attr.attributes[k] = v
    end
  end
    
  --  collect caption
  local caption = fig.caption:clone()
  fig.caption = {}
  
  -- if the image is a .tex file we need to tex \input 
  if latexIsTikzImage(fig) then
    paraEl = pandoc.walk_block(paraEl, {
      Image = function(image)
        return latexFigureInline(image, preState)
      end
    })
  end
  
  -- insert the paragraph and a caption paragraph
  figureDiv.content:insert(paraEl)
  figureDiv.content:insert(pandoc.Para(caption))
  
  -- return the div
  return figureDiv
  
end

function discoverLinkedFigureDiv(el, captionRequired)
  if el.t == "Div" and 
     hasFigureRef(el) and
     #el.content == 2 and 
     el.content[1].t == "Para" and 
     el.content[2].t == "Para" then
    return discoverLinkedFigure(el.content[1], captionRequired)  
  end
  return nil
end

local anonymousCount = 0
function anonymousFigId()
  anonymousCount = anonymousCount + 1
  return "fig-anonymous-" .. tostring(anonymousCount)
end

function isAnonymousFigId(identifier)
  return string.find(identifier, "^fig%-anonymous-")
end

function isReferenceableFig(figEl)
  return figEl.attr.identifier ~= "" and 
         not isAnonymousFigId(figEl.attr.identifier)
end



function latexIsTikzImage(image)
  return _quarto.format.isLatexOutput() and string.find(image.src, "%.tex$")
end

function latexFigureInline(image, state)
  -- if this is a tex file (e.g. created w/ tikz) then use \\input
  if latexIsTikzImage(image) then
    
    -- be sure to inject \usepackage{tikz}
    state.usingTikz = true
    
    -- base input
    local input = "\\input{" .. image.src .. "}"
    
    -- apply resize.width and/or resize.height if specified
    local rw = attribute(image, kResizeWidth, attribute(image, "width", "!"))
    local rh = attribute(image, kResizeHeight, attribute(image, "height", "!"))

    -- convert % to linewidth
    rw = asLatexSize(rw)
    rh = asLatexSize(rh)

    if rw ~= "!" or rh ~= "!" then
      input = "\\resizebox{" .. rw .. "}{" .. rh .. "}{" .. input .. "}"
    end
    
    -- return inline
    return pandoc.RawInline("latex", input)
  else
    return image
  end
end



-- file-metadata.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


fileMetadataState = {
  file = nil,
  appendix = false,
  include_directory = nil,
}


function fileMetadata() 
  return {
    RawInline = parseFileMetadata,
    RawBlock = parseFileMetadata      
  }
end

function parseFileMetadata(el)
  if _quarto.format.isRawHtml(el) then
    local rawMetadata = string.match(el.text, "^<!%-%- quarto%-file%-metadata: ([^ ]+) %-%->$")
    if rawMetadata then
      local decoded = base64_decode(rawMetadata)
      local file = quarto.json.decode(decoded)
      fileMetadataState.file = file
      -- flip into appendix mode as appropriate
      if file.bookItemType == "appendix" then
        fileMetadataState.appendix = true
      end

      -- set and unset file directory for includes
      if file.include_directory ~= nil then
        fileMetadataState.include_directory = file.include_directory
      end
      if file.clear_include_directory ~= nil then
        fileMetadataState.include_directory = nil
      end
    end
  end
  return el
end

function currentFileMetadataState()
  return fileMetadataState
end


function resetFileMetadata()  
  fileMetadataState = {
    file = nil,
    appendix = false,
    include_directory = nil,
  }
end

  
-- format.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function round(num, numDecimalPlaces)
  local mult = 10^(numDecimalPlaces or 0)
  return math.floor(num * mult + 0.5) / mult
end
-- latex.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- generates a set of options for a tColorBox
function tColorOptions(options) 

  local optionStr = ""
  local prepend = false
  for k, v in pairs(options) do
    if (prepend) then 
      optionStr = optionStr .. ', '
    end
    if v ~= "" then
      optionStr = optionStr .. k .. '=' .. v
    else
      optionStr = optionStr .. k
    end
    prepend = true
  end
  return optionStr

end
-- layout.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

kLayoutAlign = "layout-align"
kLayoutVAlign = "layout-valign"
kLayoutNcol = "layout-ncol"
kLayoutNrow = "layout-nrow"
kLayout = "layout"


function layoutAlignAttribute(el, default)
  return validatedAlign(attribute(el, kLayoutAlign, default))
end

function layoutVAlignAttribute(el, default)
  return validatedVAlign(attribute(el, kLayoutVAlign, default))
end

function hasLayoutAttributes(el)
  local attribs = tkeys(el.attr.attributes)
  return attribs:includes(kLayoutNrow) or
         attribs:includes(kLayoutNcol) or
         attribs:includes(kLayout)
end

function isLayoutAttribute(key)
  return key == kLayoutNrow or
         key == kLayoutNcol or
         key == kLayout
end

-- locate an image in a layout cell
function figureImageFromLayoutCell(cellDivEl)
  for _,block in ipairs(cellDivEl.content) do
    local fig = discoverFigure(block, false)
    if not fig then
      fig = discoverLinkedFigure(block, false)
    end
    if not fig then
      fig = discoverLinkedFigureDiv(block, false)
    end
    if fig then
      return fig
    end
  end
  return nil
end


-- we often wrap a table in a div, unwrap it
function tableFromLayoutCell(cell)
  if #cell.content == 1 and cell.content[1].t == "Table" then
    return cell.content[1]
  else
    return nil
  end
end

-- resolve alignment for layout cell (default to center or left depending
-- on the content in the cell)
function layoutCellAlignment(cell, align)
  if not align then
    local image = figureImageFromLayoutCell(cell) 
    local tbl = tableFromLayoutCell(cell)
    if image or tbl then
      return "center"
    else
      return "left"
    end
  else
    return align
  end
end

-- does the layout cell have a ref parent
function layoutCellHasRefParent(cell)
  if hasRefParent(cell) then
    return true
  else
    local image = figureImageFromLayoutCell(cell)
    if image then
      return hasRefParent(image)
    end
  end
  return false
end

function sizeToPercent(size)
  if size then
    local percent = string.match(size, "^([%d%.]+)%%$")
    if percent then
      return tonumber(percent)
    end
  end
  return nil
end

function asLatexSize(size, macro)
  -- default to linewidth
  if not macro then
    macro = "linewidth"
  end
  -- see if this is a percent, if it is the conver 
  local percentSize = sizeToPercent(size)
  if percentSize then
    if percentSize == 100 then
      return "\\" .. macro
    else
      return string.format("%2.2f", percentSize/100) .. "\\" .. macro
    end
  else
    return size
  end
end
-- license.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- read and replace the license field
-- with reshaped data that has been 
-- restructured into the standard license
-- format

local kLicense = "license"
local kCopyright = "copyright"

local kYear = "year"


local function ccLicenseUrl(type, lang) 
  local langStr = 'en'
  if lang ~= nil then
    langStr = pandoc.utils.stringify(lang)
  end
  if langStr:lower() == 'en' then
    return 'https://creativecommons.org/licenses/' .. type:lower() .. '/4.0/'
  else 
    return 'https://creativecommons.org/licenses/' .. type:lower() .. '/4.0/deed.' .. langStr:lower()
  end 
end

local licenses = {  
  ["cc by"] = {
    type = "creative-commons",
    licenseUrl = function (lang) 
      return ccLicenseUrl("by", lang)
    end
  },
  ["cc by-sa"] = {
    type = "creative-commons",
    licenseUrl = function (lang) 
      return ccLicenseUrl("by-sa", lang)
    end
  },
  ["cc by-nd"] = {
    type = "creative-commons",
    licenseUrl = function (lang) 
      return ccLicenseUrl("by-nd", lang)
    end
  },
  ["cc by-nc"] = {
    type = "creative-commons",
    licenseUrl = function (lang) 
      return ccLicenseUrl("by-nc", lang)
    end
  },
}

function processLicense(el, meta) 
  if pandoc.utils.type(el) == "Inlines" then
    local licenseStr = pandoc.utils.stringify(el)
    local license = licenses[licenseStr:lower()]
    if license ~= nil then
      return {
        type = pandoc.Inlines(license.type),
        url = pandoc.Inlines(license.licenseUrl(meta.lang)),
        text = pandoc.Inlines('')
      }
    else
      return {
        text = el
      }
    end
  else 
    return el
  end
end

function processCopyright(el) 
  if pandoc.utils.type(el) == "Inlines" then
    return {
      statement = el
    }
  else 
    if el[kYear] then
      local year = el[kYear]
      if pandoc.utils.type(year) == "Inlines" then
        local yearStr = pandoc.utils.stringify(year)
        if yearStr:find(',') then
          -- expands a comma delimited list
          local yearStrs = split(yearStr, ',')
          local yearList = pandoc.List()
          for i, v in ipairs(yearStrs) do
            yearList:insert(pandoc.Inlines(v))
          end
          el[kYear] = yearList
        elseif yearStr:find('-') then
          -- expands a range
          local years = split(yearStr, '-')

          -- must be exactly two years in the range
          if #years == 2 then
            local start = tonumber(years[1])
            local finish = tonumber(years[2])

            -- if they're in the wrong order, just fix it
            if start > finish then
              local oldstart = start
              start = finish
              finish = oldstart
            end
            
            -- make the list of years
            local yearList = pandoc.List()
            for i=start,finish do
              yearList:insert(pandoc.Inlines(string.format("%.0f",i)))
            end
            el[kYear] = yearList
          end
        end

      end
    end
    return el
  end
end

function processLicenseMeta(meta)
  if meta then
    local licenseMeta = meta[kLicense]
    if licenseMeta then
      if pandoc.utils.type(licenseMeta) == "List" then
        local normalizedEls = {}
        for i,v in ipairs(licenseMeta) do        
          local normalized = processLicense(v, meta)
          tappend(normalizedEls, {normalized})
        end
        meta[kLicense] = normalizedEls
      elseif pandoc.utils.type(licenseMeta) == "Inlines" then
        meta[kLicense] = {processLicense(licenseMeta, meta)}
      end
    end


    local copyrightMeta = meta[kCopyright]
    if copyrightMeta then
        meta[kCopyright] = processCopyright(copyrightMeta)
    end
  end
  return meta
end

-- list.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function filter(list, test) 
  local result = {}
  for index, value in ipairs(list) do
      if test(value, index) then
          result[#result + 1] = value
      end
  end
  return result
end

-- log.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- TODO
-- could write to named filed (e.g. <docname>.filter.log) and client could read warnings and delete (also delete before run)
-- always append b/c multiple filters

function info(message)
  io.stderr:write(message .. "\n")
end

function warn(message) 
  io.stderr:write(lunacolors.yellow("WARNING: " .. message .. "\n"))
end

function error(message)
  io.stderr:write(lunacolors.red("ERROR: " .. message .. "\n"))
end

-- lunacolors.lua
--
-- Copyright (c) 2021, Hilbis
-- https://github.com/Rosettea/Lunacolors

lunacolors = {}

function init(name, codes)
	lunacolors[name] = function(text)
		return ansi(codes[1], codes[2], text)
	end
end

function ansi(open, close, text)
	if text == nil then return '\27[' .. open .. 'm' end
	return '\27[' .. open .. 'm' .. text .. '\27[' .. close .. 'm'
end

-- Define colors
-- Modifiers
init('reset', {0, 0})
init('bold', {1, 22})
init('dim', {2, 22})
init('italic', {3, 23})
init('underline', {4, 24})
init('invert', {7, 27})
init('hidden', {8, 28})
init('strikethrough', {9, 29})

-- Colors
init('black', {30, 39})
init('red', {31, 39})
init('green', {32, 39})
init('yellow', {33, 39})
init('blue', {34, 39})
init('magenta', {35, 39})
init('cyan', {36, 39})
init('white', {37, 39})

-- Background colors
init('blackBg', {40, 49})
init('redBg', {41, 49})
init('greenBg', {42, 49})
init('yellowBg', {43, 49})
init('blueBg', {44, 49})
init('magentaBg', {45, 49})
init('cyanBg', {46, 49})
init('whiteBg', {47, 49})

-- Bright colors
init('brightBlack', {90, 39})
init('brightRed', {91, 39})
init('brightGreen', {92, 39})
init('brightYellow', {93, 39})
init('brightBlue', {94, 39})
init('brightMagenta', {95, 39})
init('brightCyan', {96, 39})
init('brightWhite', {97, 39})

-- Bright background 
init('brightBlackBg', {100, 49})
init('brightRedBg', {101, 49})
init('brightGreenBg', {102, 49})
init('brightYellowBg', {103, 49})
init('brightBlueBg', {104, 49})
init('brightMagentaBg', {105, 49})
init('brightCyanBg', {106, 49})
init('brightWhiteBg', {107, 49})

lunacolors.version = '0.1.0'
lunacolors.format = function(text)
	local colors = {
		reset = {'{reset}', ansi(0)},
		bold = {'{bold}', ansi(1)},
		dim = {'{dim}', ansi(2)},
		italic = {'{italic}', ansi(3)},
		underline = {'{underline}', ansi(4)},
		invert = {'{invert}', ansi(7)},
		bold_off = {'{bold-off}', ansi(22)},
		underline_off = {'{underline-off}', ansi(24)},
		black = {'{black}', ansi(30)},
		red = {'{red}', ansi(31)},
		green = {'{green}', ansi(32)},
		yellow = {'{yellow}', ansi(33)},
		blue = {'{blue}', ansi(34)},
		magenta = {'{magenta}', ansi(35)},
		cyan = {'{cyan}', ansi(36)},
		white = {'{white}', ansi(37)},
		red_bg = {'{red-bg}', ansi(41)},
		green_bg = {'{green-bg}', ansi(42)},
		yellow_bg = {'{green-bg}', ansi(43)},
		blue_bg = {'{blue-bg}', ansi(44)},
		magenta_bg = {'{magenta-bg}', ansi(45)},
		cyan_bg = {'{cyan-bg}', ansi(46)},
		white_bg = {'{white-bg}', ansi(47)},
		gray = {'{gray}', ansi(90)},
		bright_red = {'{bright-red}', ansi(91)},
		bright_green = {'{bright-green}', ansi(92)},
		bright_yellow = {'{bright-yellow}', ansi(93)},
		bright_blue = {'{bright-blue}', ansi(94)},
		bright_magenta = {'{bright-magenta}', ansi(95)},
		bright_cyan = {'{bright-cyan}', ansi(96)}
	}

	for k, v in pairs(colors) do
		text = text:gsub(v[1], v[2])
	end

	return text .. colors['reset'][2]
end
-- map-or-call.lua
-- Copyright (C) 2020 by RStudio, PBC

function map_or_call(fun, arrayOrValue)
  if tisarray(arrayOrValue) then
    -- array
    local result = {}
    for i, v in pairs(arrayOrValue) do
      table.insert(result, fun(v))
    end
    return result
  else
    -- value
    return fun(arrayOrValue)
  end
end
-- meta.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- constants
kHeaderIncludes = "header-includes"
kIncludeBefore = "include-before"
kIncludeAfter = "include-after"

function ensureIncludes(meta, includes)
  if not meta[includes] then
    meta[includes] = pandoc.List({})
  elseif pandoc.utils.type(meta[includes]) == "Inlines" or 
         pandoc.utils.type(meta[includes]) == "Blocks" then
    meta[includes] = pandoc.List({meta[includes]})
  end
end

function removeEmptyIncludes(meta, includes)
  if meta[includes] and 
     pandoc.utils.type(meta[includes]) == "List" and
     #meta[includes] == 0 then
    meta[includes] = nil
  end
end

function removeAllEmptyIncludes(meta)
  removeEmptyIncludes(meta, kHeaderIncludes)
  removeEmptyIncludes(meta, kIncludeBefore)
  removeEmptyIncludes(meta, kIncludeAfter)
end

-- add a header include as a raw block
function addInclude(meta, format, includes, include)
  if _quarto.format.isHtmlOutput() then
    blockFormat = "html"
  else
    blockFormat = format
  end  
  meta[includes]:insert(pandoc.Blocks({ pandoc.RawBlock(blockFormat, include) }))
end

-- conditionally include a package
function usePackage(pkg)
  return "\\@ifpackageloaded{" .. pkg .. "}{}{\\usepackage{" .. pkg .. "}}"
end

function usePackageWithOption(pkg, option)
  return "\\@ifpackageloaded{" .. pkg .. "}{}{\\usepackage[" .. option .. "]{" .. pkg .. "}}"
end

function metaInjectLatex(meta, func)
  if _quarto.format.isLatexOutput() then
    local function inject(tex)
      addInclude(meta, "tex", kHeaderIncludes, tex)
    end
    inject("\\makeatletter")
    func(inject)
    inject("\\makeatother")
  end
end

function metaInjectLatexBefore(meta, func)
  metaInjectRawLatex(meta, kIncludeBefore, func)
end

function metaInjectLatexAfter(meta, func)
  metaInjectRawLatex(meta, kIncludeAfter, func)
end

function metaInjectRawLatex(meta, include, func)
  if _quarto.format.isLatexOutput() then
    local function inject(tex)
      addInclude(meta, "tex", include, tex)
    end
    func(inject)
  end
end


function metaInjectHtml(meta, func)
  if _quarto.format.isHtmlOutput() then
    local function inject(html)
      addInclude(meta, "html", kHeaderIncludes, html)
    end
    func(inject)
  end
end


function readMetaOptions(meta) 
  local options = {}
  for key,value in pairs(meta) do
    if type(value) == "table" and value.clone ~= nil then
      options[key] = value:clone()
    else
      options[key] = value
    end 
  end
  return options
end
-- options.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- initialize options from 'crossref' metadata value
function readFilterOptions(meta, filter)
  local options = {}
  if type(meta[filter]) == "table" then
    options = readMetaOptions(meta[filter])
  end
  return options
end

-- get option value
function readOption(options, name, default)
  local value = options[name]
  if value == nil then
    value = default
  end

  if type(value) == "table" and value.clone ~= nil then
    return value:clone()
  else
    return value;
  end
end



-- pandoc.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function hasBootstrap() 
  local hasBootstrap = param("has-bootstrap", false)
  return hasBootstrap
end


-- read attribute w/ default
function attribute(el, name, default)
  for k,v in pairs(el.attr.attributes) do
    if k == name then
      return v
    end
  end
  return default
end

function removeClass(classes, remove)
  return classes:filter(function(clz) return clz ~= remove end)
end

function combineFilters(filters) 

  -- the final list of filters
  local filterList = {}
  for _, filter in ipairs(filters) do
    for key,func in pairs(filter) do

      -- ensure that there is a list for this key
      if filterList[key] == nil then
        filterList[key] = pandoc.List()
      end

      -- add the current function to the list
      filterList[key]:insert(func)
    end
  end

  local combinedFilters = {}
  for key,fns in pairs(filterList) do

    -- if there is only one function for this key
    -- just use it
    if #fns == 1 then
      combinedFilters[key] = fns[1]
    else
      -- otherwise combine them into a single function
      combinedFilters[key] = function(x) 
        -- capture the current value
        local current = x

        -- iterate through functions for this key
        for _, fn in ipairs(fns) do
          local result = fn(current)
          if result ~= nil then
            -- if there is a result from this function
            -- update the current value with the result
            current = result
          end
        end

        -- return result from calling the functions
        return current
      end
    end
  end
  return combinedFilters
end

function inlinesToString(inlines)
  return pandoc.utils.stringify(pandoc.Span(inlines))
end

-- lua string to pandoc inlines
function stringToInlines(str)
  if str then
    return pandoc.List({pandoc.Str(str)})
  else
    return pandoc.List({})
  end
end

-- lua string with markdown to pandoc inlines
function markdownToInlines(str)
  if str then
    local doc = pandoc.read(str)
    return doc.blocks[1].content
  else
    return pandoc.List()
  end
end

function stripTrailingSpace(inlines)
  -- we always convert to pandoc.List to ensure a uniform
  -- return type (and its associated methods)
  if #inlines > 0 then
    if inlines[#inlines].t == "Space" then
      return pandoc.List(tslice(inlines, 1, #inlines - 1))
    else
      return pandoc.List(inlines)
    end
  else
    return pandoc.List(inlines)
  end
end

-- non-breaking space
function nbspString()
  return pandoc.Str '\u{a0}'
end

-- the first heading in a div is sometimes the caption
function resolveHeadingCaption(div) 
  local capEl = div.content[1]
  if capEl ~= nil and capEl.t == 'Header' then
    div.content:remove(1)
    return capEl.content
  else 
    return nil
  end
end

local kBlockTypes = {
  "BlockQuote",
  "BulletList", 
  "CodeBlock ",
  "DefinitionList",
  "Div",
  "Header",
  "HorizontalRule",
  "LineBlock",
  "OrderedList",
  "Para",
  "Plain",
  "RawBlock",
  "Table"
}

function isBlockEl(el)
  return tcontains(kBlockTypes, el.t)
end

function isInlineEl(el)
  return not isBlockEl(el)
end

function compileTemplate(template, meta)
  local f = io.open(pandoc.utils.stringify(template), "r")
  if f then
    local contents = f:read("*all")
    f:close()
    -- compile the title block template
    local compiledTemplate = pandoc.template.compile(contents)
    local template_opts = pandoc.WriterOptions {template = compiledTemplate}  

    -- render the current document and read it to generate an AST for the
    -- title block
    local metaDoc = pandoc.Pandoc(pandoc.Blocks({}), meta)
    local rendered = pandoc.write(metaDoc, 'gfm', template_opts)

    -- read the rendered document 
    local renderedDoc = pandoc.read(rendered, 'gfm')

    return renderedDoc.blocks
  else
    fail('Error compiling template: ' .. template)
  end
end

-- paths.lua
-- Copyright (C) 2022 Posit Software, PBC

function resourceRef(ref, dir)
  -- if the ref starts with / then just strip if off
  if string.find(ref, "^/") then
    -- check for protocol relative url
    if string.find(ref, "^//") == nil then
      return pandoc.text.sub(ref, 2, #ref)
    else
      return ref
    end
  -- if it's a relative ref then prepend the resource dir
  elseif isRelativeRef(ref) then
    if dir == '.' then
      return ref
    else
      return dir .. "/" .. ref
    end
  else
  -- otherwise just return it
    return ref
  end
end

function fixIncludePath(ref, dir)
  -- if it's a relative ref then prepend the resource dir
  if isRelativeRef(ref) then
    if dir ~= "." then
      return dir .. "/" .. ref
    else
      return ref
    end
  else
  -- otherwise just return it
    return ref
  end
end


function isRelativeRef(ref)
  return ref:find("^/") == nil and 
         ref:find("^%a+://") == nil and 
         ref:find("^data:") == nil and 
         ref:find("^#") == nil
end



function handlePaths(el, path, replacer)
  el.text = handleHtmlRefs(el.text, path, "img", "src", replacer)
  el.text = handleHtmlRefs(el.text, path, "img", "data-src", replacer)
  el.text = handleHtmlRefs(el.text, path, "link", "href", replacer)
  el.text = handleHtmlRefs(el.text, path, "script", "src", replacer)
  el.text = handleHtmlRefs(el.text, path, "source", "src", replacer)
  el.text = handleHtmlRefs(el.text, path, "embed", "src", replacer)
  el.text = handleCssRefs(el.text, path, "@import%s+", replacer)
  el.text = handleCssRefs(el.text, path, "url%(", replacer)
end


function handleHtmlRefs(text, resourceDir, tag, attrib, replacer)
  return text:gsub("(<" .. tag .. " [^>]*" .. attrib .. "%s*=%s*)\"([^\"]+)\"", function(preface, value)
    return preface .. "\"" .. replacer(value, resourceDir) .. "\""
  end)
end

function handleCssRefs(text, resourceDir, prefix, replacer)
  return text:gsub("(" .. prefix .. ")\"([^\"]+)\"", function(preface, value)
    return preface .. "\"" .. replacer(value, resourceDir) .. "\""
  end) 
end


-- ref parent attribute (e.g. fig:parent or tbl:parent)
kRefParent = "ref-parent"


-- does this element have a figure label?
function hasFigureRef(el)
  return isFigureRef(el.attr.identifier)
end

function isFigureRef(identifier)
  return (identifier ~= nil) and string.find(identifier, "^fig%-")
end

-- does this element have a table label?
function hasTableRef(el)
  return isTableRef(el.attr.identifier)
end

function isTableRef(identifier)
  return (identifier ~= nil) and string.find(identifier, "^tbl%-")
end

-- does this element support sub-references
function hasFigureOrTableRef(el)
  return el.attr and (hasFigureRef(el) or hasTableRef(el))
end


function isRefParent(el)
  return el.t == "Div" and 
         (hasFigureRef(el) or hasTableRef(el)) and
         refCaptionFromDiv(el) ~= nil
end

function hasRefParent(el)
  return el.attr.attributes[kRefParent] ~= nil
end

function refType(id)
  local match = string.match(id, "^(%a+)%-")
  if match then
    return pandoc.text.lower(match)
  else
    return nil
  end
end

function refCaptionFromDiv(el)
  local last = el.content[#el.content]
  if last and last.t == "Para" and #el.content > 1 then
    return last
  else
    return nil
  end
end

function noCaption()
  return pandoc.Strong( { pandoc.Str("?(caption)") })
end

function emptyCaption()
  return pandoc.Str("")
end

function hasSubRefs(divEl, type)
  if hasFigureOrTableRef(divEl) and not hasRefParent(divEl) then
    -- children w/ parent id
    local found = false
    local function checkForParent(el)
      if not found then
        if hasRefParent(el) then
          if not type or (refType(el.attr.identifier) == type) then
            found = true
          end
        end

      end
    end
    _quarto.ast.walk(divEl, {
      Div = checkForParent,
      Image = checkForParent
    })
    return found
  else
    return false
  end
end
   


-- string.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


-- tests whether a string ends with another string
function endsWith(str, ending) 
  return ending == "" or str:sub(-#ending) == ending
end

function startsWith(str, starting) 
  return starting == "" or str:sub(1, #starting) == starting
end

-- trim a string
function trim(s)
  return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end

-- splits a string on a separator
function split(str, sep)
  local fields = {}
  
  local sep = sep or " "
  local pattern = string.format("([^%s]+)", sep)
  local _ignored = string.gsub(str, pattern, function(c) fields[#fields + 1] = c end)
  
  return fields
end

-- escape string by converting using Pandoc
function stringEscape(str, format)
  local doc = pandoc.Pandoc({pandoc.Para(str)})
  return pandoc.write(doc, format)
end

-- The character `%´ works as an escape for those magic characters. 
-- So, '%.' matches a dot; '%%' matches the character `%´ itself. 
-- You can use the escape `%´ not only for the magic characters, 
-- but also for all other non-alphanumeric characters. When in doubt, 
-- play safe and put an escape.
-- ( from http://www.lua.org/pil/20.2.html )
function patternEscape(str) 
  return str:gsub("([^%w])", "%%%1")
end
-- table.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- append values to table
function tappend(t, values)
  for i,value in pairs(values) do
    table.insert(t, value)
  end
end

-- prepend values to table
function tprepend(t, values)
  for i=1, #values do
   table.insert(t, 1, values[#values + 1 - i])
  end
end

-- slice elements out of a table
function tslice(t, first, last, step)
  local sliced = {}
  for i = first or 1, last or #t, step or 1 do
    sliced[#sliced+1] = t[i]
  end
  return sliced
end

-- is the table a simple array?
-- see: https://web.archive.org/web/20140227143701/http://ericjmritz.name/2014/02/26/lua-is_array/
function tisarray(t)
  if type(t) ~= "table" then 
    return false 
  end
  local i = 0
  for _ in pairs(t) do
      i = i + 1
      if t[i] == nil then return false end
  end
  return true
end

-- map elements of a table
function tmap(tbl, f)
  local t = {}
  for k,v in pairs(tbl) do
      t[k] = f(v)
  end
  return t
end

-- does the table contain a value
function tcontains(t,value)
  if t and type(t)=="table" and value then
    for _, v in ipairs (t) do
      if v == value then
        return true
      end
    end
    return false
  end
  return false
end

-- clear a table
function tclear(t)
  for k,v in pairs(t) do
    t[k] = nil
  end
end

-- get keys from table
function tkeys(t)
  local keyset=pandoc.List({})
  local n=0
  for k,v in pairs(t) do
    n=n+1
    keyset[n]=k
  end
  return keyset
end

-- sorted pairs. order function takes (t, a,)
function spairs(t, order)
  -- collect the keys
  local keys = {}
  for k in pairs(t) do keys[#keys+1] = k end

  -- if order function given, sort by it by passing the table and keys a, b,
  -- otherwise just sort the keys
  if order then
      table.sort(keys, function(a,b) return order(t, a, b) end)
  else
      table.sort(keys)
  end

  -- return the iterator function
  local i = 0
  return function()
      i = i + 1
      if keys[i] then
          return keys[i], t[keys[i]]
      end
  end
end
-- tables.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function htmlTableCaptionPattern()
  return tagPattern("[Cc][Aa][Pp][Tt][Ii][Oo][Nn]")
end

function htmlTableTagNamePattern()
  return "[Tt][Aa][Bb][Ll][Ee]"
end

function htmlTablePattern()
  return tagPattern(htmlTableTagNamePattern())
end

function htmlPagedTablePattern()
  return "<script data[-]pagedtable[-]source type=\"application/json\">"
end

function htmlGtTablePattern()
  return "<table class=\"gt_table\">"
end

function tagPattern(tag)
  local pattern = "(<" .. tag .. "[^>]*>)(.*)(</" .. tag .. ">)"
  return pattern
end

function anonymousTblId()
  return "tbl-anonymous-" .. tostring(math.random(10000000))
end

function isAnonymousTblId(identifier)
  return string.find(identifier, "^tbl%-anonymous-")
end

function isReferenceableTbl(tblEl)
  return tblEl.attr.identifier ~= "" and 
         not isAnonymousTblId(tblEl.attr.identifier)
end


function parseTableCaption(caption)
  -- string trailing space
  caption = stripTrailingSpace(caption)
  -- does the caption end with "}"
  local lastInline = caption[#caption]
  if lastInline.t == "Str" then
    if endsWith(trim(lastInline.text), "}") then
      -- find index of first inline that starts with "{"
      local beginIndex = nil
      for i = 1,#caption do 
        if caption[i].t == "Str" and startsWith(caption[i].text, "{") then
          beginIndex = i
          break
        end
      end
      if beginIndex ~= nil then 
        local attrText = trim(inlinesToString(tslice(caption, beginIndex, #caption)))
        attrText = attrText:gsub("“", "'"):gsub("”", "'")
        local elWithAttr = pandoc.read("## " .. attrText).blocks[1]
        if elWithAttr.attr ~= nil then
          if not startsWith(attrText, "{#") then
            elWithAttr.attr.identifier = ""
          end
          if beginIndex > 1 then
            return stripTrailingSpace(tslice(caption, 1, beginIndex - 1)), elWithAttr.attr
          else
            return pandoc.List({}), elWithAttr.attr
          end
        end
      end
    end   
  end

  -- no attributes
  return caption, pandoc.Attr("")

end

function createTableCaption(caption, attr)
  -- convert attr to inlines
  local attrInlines = pandoc.List()
  if attr.identifier ~= nil and attr.identifier ~= "" then
    attrInlines:insert(pandoc.Str("#" .. attr.identifier))
  end
  if #attr.classes > 0 then
    for i = 1,#attr.classes do
      if #attrInlines > 0 then
        attrInlines:insert(pandoc.Space())
      end
      attrInlines:insert(pandoc.Str("." .. attr.classes[i]))
    end
  end
  if #attr.attributes > 0 then
    for k,v in pairs(attr.attributes) do
      if #attrInlines > 0 then
        attrInlines:insert(pandoc.Space())
      end
      attrInlines:insert(pandoc.Str(k .. "='" .. v .. "'"))
    end
  end
  if #attrInlines > 0 then
    attrInlines:insert(1, pandoc.Space())
    attrInlines[2] = pandoc.Str("{" .. attrInlines[2].text)
    attrInlines[#attrInlines] = pandoc.Str(attrInlines[#attrInlines].text .. "}")
    local tableCaption = caption:clone()
    tappend(tableCaption, attrInlines)
    return tableCaption
  else
    return caption
  end
end


function countTables(div)
  local tables = 0
  _quarto.ast.walk(div, {
    Table = function(table)
      tables = tables + 1
    end,
    RawBlock = function(raw)
      if hasTable(raw) then
        tables = tables + 1
      end
    end
  })
  return tables
end

function hasGtHtmlTable(raw)
  if _quarto.format.isRawHtml(raw) and _quarto.format.isHtmlOutput() then
    return raw.text:match(htmlGtTablePattern())
  else
    return false
  end
end

function hasPagedHtmlTable(raw)
  if _quarto.format.isRawHtml(raw) and _quarto.format.isHtmlOutput() then
    return raw.text:match(htmlPagedTablePattern())
  else
    return false
  end
end

function hasRawHtmlTable(raw)
  if _quarto.format.isRawHtml(raw) and _quarto.format.isHtmlOutput() then
    return raw.text:match(htmlTablePattern())
  else
    return false
  end
end

function hasRawLatexTable(raw)
  if _quarto.format.isRawLatex(raw) and _quarto.format.isLatexOutput() then
    for i,pattern in ipairs(_quarto.patterns.latexTablePatterns) do
      if raw.text:match(pattern) then
        return true
      end
    end
    return false
  else
    return false
  end
end

local tableCheckers = {
  hasRawHtmlTable,
  hasRawLatexTable,
  hasPagedHtmlTable,
}

function hasTable(raw)
  for i, checker in ipairs(tableCheckers) do
    local val = checker(raw)
    if val then
      return true
    end
  end
  return false
end
-- theorems.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- available theorem types
theoremTypes = {
  thm = {
    env = "theorem",
    style = "plain",
    title = "Theorem"
  },
  lem = {
    env = "lemma",
    style = "plain",
    title = "Lemma"
  },
  cor = {
    env = "corollary",
    style = "plain",
    title = "Corollary",
  },
  prp = {
    env = "proposition",
    style = "plain",
    title = "Proposition",
  },
  cnj = {
    env = "conjecture",
    style = "plain",
    title = "Conjecture"
  },
  def = {
    env = "definition",
    style = "definition",
    title = "Definition",
  },
  exm = {
    env = "example",
    style = "definition",
    title = "Example",
  },
  exr  = {
    env = "exercise",
    style = "definition",
    title = "Exercise"
  }
}

function hasTheoremRef(el)
  local type = refType(el.attr.identifier)
  return theoremTypes[type] ~= nil
end

proofTypes = {
  proof =  {
    env = 'proof',
    title = 'Proof'
  },
  remark =  {
    env = 'remark',
    title = 'Remark'
  },
  solution = {
    env = 'solution',
    title = 'Solution'
  }
}

function proofType(el)
  local type = el.attr.classes:find_if(function(clz) return proofTypes[clz] ~= nil end)
  if type ~= nil then
    return proofTypes[type]
  else
    return nil
  end

end
-- timing.lua
-- Copyright (C) 2022 Posit Software, PBC

-- https://stackoverflow.com/questions/463101/lua-current-time-in-milliseconds :(
function get_current_time()
  -- FIXME this will not necessarily work on windows..
  local handle = io.popen("python -c 'import time; print(time.time() * 1000)'")
  if handle then
    local result = tonumber(handle:read("*a"))
    handle:close()
    return result
  else
    fail('Error reading current time')
  end
end

if os.getenv("QUARTO_PROFILER_OUTPUT") ~= nil then
  timing_events = { { name = "_start", time = get_current_time() } }
else
  timing_events = {}
end

function register_time(event_name)
  local t = get_current_time()
  table.insert(timing_events, { 
    time = t,
    name = event_name
  })
end

function capture_timings(filterList, trace)
  local finalResult = {}

  if os.getenv("QUARTO_PROFILER_OUTPUT") ~= nil then
    for i, v in ipairs(filterList) do
      local newFilter = {}
      newFilter._filter_name = v["name"]

      local oldPandoc = v["filter"]["Pandoc"]
      for key,func in pairs(v) do
        newFilter[key] = func
      end
      local function makeNewFilter(oldPandoc)
        return function (p)
          if oldPandoc ~= nil then
            local result = oldPandoc(p)
            register_time(v["name"])
            return result
          else
            register_time(v["name"])
          end
        end 
      end
      newFilter["Pandoc"] = makeNewFilter(oldPandoc) -- iife for capturing in scope
      if trace then
        table.insert(finalResult, trace_filter(string.format("%02d_%s.json", i, v.name), newFilter))
      else
        table.insert(finalResult, newFilter)
      end
    end
  else
    for i, v in ipairs(filterList) do
      if v.filter ~= nil then
        v.filter._filter_name = v.name
        if trace then
          table.insert(finalResult, trace_filter(string.format("%02d_%s.json", i, v.name), v.filter))
        else
          table.insert(finalResult, v.filter)
        end
      elseif v.filters ~= nil then
        for j, innerV in pairs(v.filters) do
          innerV._filter_name = string.format("%s-%s", v.name, j)
          if trace then
            table.insert(finalResult, trace_filter(string.format("%02d_%02d_%s.json", i, j, innerV._filter_name), innerV))
          else
            table.insert(finalResult, innerV)
          end
        end
      else
        print("Warning: filter " .. v.name .. " didn't declare filter or filters.")
      end
    end
  end

  return finalResult
end
-- url.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function urldecode(url)
  if url == nil then
  return
  end
    url = url:gsub("+", " ")
    url = url:gsub("%%(%x%x)", function(x)
      return string.char(tonumber(x, 16))
    end)
  return url
end

function fullyUrlDecode(url)
  -- decode the url until it is fully decoded (not a single pass,
  -- but repeated until it decodes no further)
  result = urldecode(url)
  if result == url then
    return result
  else 
    return fullyUrlDecode(result)
  end
end
-- validate.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

kAlignments = pandoc.List({ "center", "left", "right" })
kVAlignments = pandoc.List({"top", "center", "bottom"})

function validatedAlign(align)
  return validateInList(align, kAlignments, "alignment", "center")
end

function validatedVAlign(vAlign)
  return validateInList(vAlign, kVAlignments, "vertical alignment", "top")
end

function validateInList(value, list, attribute, default)
  if value == "default" then
    return default
  elseif value and not list:includes(value) then
    warn("Invalid " .. attribute .. " attribute value: " .. value)
    return default
  elseif value then
    return value
  else
    return default
  end
end


-- wrapped-filter.lua
-- creates wrapped pandoc filters
-- Copyright (C) 2022 by RStudio, PBC

local function shortcodeMetatable(scriptFile) 
  return {
    -- https://www.lua.org/manual/5.3/manual.html#6.1
    assert = assert,
    collectgarbage = collectgarbage,
    dofile = dofile,
    error = error,
    getmetatable = getmetatable,
    ipairs = ipairs,
    load = load,
    loadfile = loadfile,
    next = next,
    pairs = pairs,
    pcall = pcall,
    print = print,
    rawequal = rawequal,
    rawget = rawget,
    rawlen = rawlen,
    rawset = rawset,
    select = select,
    setmetatable = setmetatable,
    tonumber = tonumber,
    tostring = tostring,
    type = type,
    _VERSION = _VERSION,
    xpcall = xpcall,
    coroutine = coroutine,
    require = require,
    package = package,
    string = string,
    utf8 = utf8,
    table = table,
    math = math,
    io = io,
    file = file,
    os = os,
    debug = debug,
    -- https://pandoc.org/lua-filters.html
    FORMAT = FORMAT,
    PANDOC_READER_OPTIONS = PANDOC_READER_OPTIONS,
    PANDOC_WRITER_OPTIONS = PANDOC_WRITER_OPTIONS,
    PANDOC_VERSION = PANDOC_VERSION,
    PANDOC_API_VERSION = PANDOC_API_VERSION,
    PANDOC_SCRIPT_FILE = scriptFile,
    PANDOC_STATE = PANDOC_STATE,
    pandoc = pandoc,
    lpeg = lpeg,
    re = re,
    -- quarto global environment
    json = json,
    -- quarto functions
    quarto = quarto,
    -- global environment
    _G = _G
  }
end

local function safeguard_for_meta(customnode)
  if customnode == nil then
    return nil
  end
  local result = {}
  for k,v in pairs(customnode) do
    local t = type(v)
    local pt = pandoc.utils.type(v)
    if pt == "Attr" then
      local converted_attrs = {}
      for i, attr in ipairs(v.attributes) do
        table.insert(converted_attrs, {
          attr[1], attr[2]
        })
      end
      result[k] = {
        identifier = v.identifier,
        classes = v.classes,
        attributes = converted_attrs
      }
    elseif t == "userdata" then
      result[k] = v -- assume other pandoc objects are ok
    elseif t == "table" then
      result[k] = safeguard_for_meta(v)
    end
  end
  return result
end

function makeWrappedJsonFilter(scriptFile, filterHandler)
  local handlers = {
    Pandoc = {
      file = scriptFile,
      handle = function(doc)
        local json = pandoc.write(doc, "json")
        path = quarto.utils.resolve_path_relative_to_document(scriptFile)
        local custom_node_map = {}
        local has_custom_nodes = false
        doc = doc:walk({
          RawInline = function(raw)
            local custom_node, t, kind = _quarto.ast.resolve_custom_data(raw)
            if custom_node ~= nil then
              has_custom_nodes = true
              custom_node = safeguard_for_meta(custom_node)
              table.insert(custom_node_map, { id = raw.text, tbl = custom_node, t = t, kind = kind })
            end
          end,
          Meta = function(meta)
            if has_custom_nodes then
              meta["quarto-custom-nodes"] = pandoc.MetaList(custom_node_map)
            end
            return meta
          end
        })
        local result = pandoc.utils.run_json_filter(doc, path)
        if has_custom_nodes then
          doc:walk({
            Meta = function(meta)
              _quarto.ast.reset_custom_tbl(meta["quarto-custom-nodes"])
            end
          })
        end

        return result
      end
    }
  }

  if filterHandler ~= nil then
    return filterHandler(handlers)
  else
    local result = {}
    for k,v in pairs(handlers) do
      result[k] = v.handle
    end
    return result
  end    
end

function makeWrappedLuaFilter(scriptFile, filterHandler)
  local working_directory = pandoc.path.directory(scriptFile)
  return _quarto.withScriptFile(scriptFile, function()
    local env = setmetatable({}, {__index = shortcodeMetatable(scriptFile)})
    local chunk, err = loadfile(scriptFile, "bt", env)
    local handlers = {}
  
    local function makeSingleHandler(handlerTable)
      local result = {}
      setmetatable(result, {
        __index = { scriptFile = scriptFile }
      })
      for k,v in pairs(handlerTable) do
        result[k] = {
          file = scriptFile,
          handle = v,
        }
      end
      return result
    end
  
    if not err and chunk then
      local result = chunk()
      if result then
        if quarto.utils.table.isarray(result) then
          for i, handlerTable in ipairs(result) do
            table.insert(handlers, makeSingleHandler(handlerTable))
          end
        else
          handlers = makeSingleHandler(result)
        end
      else
        handlers = makeSingleHandler(env)
      end
  
      if filterHandler ~= nil then
        return filterHandler(handlers)
      else
        result = {}
        for k,v in pairs(handlers) do
          result[k] = v.handle
        end
        return result
      end    
    else
      error(err)
      os.exit(1)
    end
  end)
end

function makeWrappedFilter(scriptFile, filterHandler)
  if type(scriptFile) == "userdata" then
    scriptFile = pandoc.utils.stringify(scriptFile)
  end

  if type(scriptFile) == "string" then
    return makeWrappedLuaFilter(scriptFile, filterHandler)
  elseif type(scriptFile) == "table" then
    local path = scriptFile.path
    local type = scriptFile.type

    if type == "json" then
      return makeWrappedJsonFilter(path, filterHandler)  
    else
      return makeWrappedLuaFilter(path, filterHandler)
    end
  end
end

function filterIf(condition, filter)
  return {
    Pandoc = function(doc)
      if condition() then
        return doc:walk(filter)
      end
    end
  }
end

function filterSeq(filters)
  return {
    Pandoc = function(doc)
      local result
      -- TODO handle timing and tracing uniformly through our new filter infra
      for _, filter in ipairs(filters) do
        if filter.filter ~= nil then
          filter = filter.filter
        end
        local r = run_emulated_filter(doc, filter, true)
        if r ~= nil then
          doc = r
          result = r
        end
      end
      return result
    end
  }
end
-- configurefilters.lua
-- Determine which filter chains will be active

function configureFilters()
  return {
    Meta = function(meta)
      preState.active_filters = param("active-filters")
    end
  }
end
-- includes.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

kIncludeBeforeBody = "include-before-body"
kIncludeAfterBody = "include-after-body"
kIncludeInHeader = "include-in-header"

function readIncludes()
  return {
    Meta = function(meta)
      -- ensure all includes are meta lists
      ensureIncludes(meta, kHeaderIncludes)
      ensureIncludes(meta, kIncludeBefore)
      ensureIncludes(meta, kIncludeAfter)
          
      -- read file includes
      readIncludeFiles(meta, kIncludeInHeader, kHeaderIncludes)
      readIncludeFiles(meta, kIncludeBeforeBody, kIncludeBefore)
      readIncludeFiles(meta, kIncludeAfterBody, kIncludeAfter)

      -- read text based includes
      readIncludeStrings(meta, kHeaderIncludes)
      readIncludeStrings(meta, kIncludeBefore)
      readIncludeStrings(meta, kIncludeAfter)
     
      return meta
    end
  }
end

function readIncludeStrings(meta, includes)
  local strs = param(includes, {})
  for _,str in ipairs(strs) do
    if pandoc.utils.type(str) == "Blocks" then
      meta[includes]:insert(str)
    else
      if type(str) == "table" then
        str = inlinesToString(str)
      end
      addInclude(meta, FORMAT, includes, str)
    end
   
  end
end

function readIncludeFiles(meta, includes, target)

  -- process include files
  local files = param(includes, {})
  for _,file in ipairs(files) do

    local status, err = pcall(function () 
      -- read file contents
      local f = io.open(pandoc.utils.stringify(file), "r")
      if f == nil then 
        error("Error resolving " .. target .. "- unable to open file " .. file)
        os.exit(1)
      end
      local contents = f:read("*all")
      f:close()
      -- write as as raw include
      addInclude(meta, FORMAT, target, contents)
    end)
  end

  
end
-- resourceRefs.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function resourceRefs() 
  
  return {
    Image = function(el)
      local file = currentFileMetadataState().file
      if file ~= nil and file.resourceDir ~= nil then
        el.src = resourceRef(el.src, file.resourceDir)
      end
      return el
    end,

    RawInline = handleRawElementResourceRef,
    RawBlock = handleRawElementResourceRef,
  }
end

function handleRawElementResourceRef(el)
  if _quarto.format.isRawHtml(el) then
    local file = currentFileMetadataState().file
    if file ~= nil and file.resourceDir ~= nil then
      handlePaths(el, file.resourceDir, resourceRef)
      return el
    end
  end
end
-- render-asciidoc.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


local kAsciidocNativeCites = 'use-asciidoc-native-cites'

function renderAsciidoc()   

  -- This only applies to asciidoc output
  if not quarto.doc.isFormat("asciidoc") then
    return {}
  end

  local hasMath = false

  return {
    Meta = function(meta)
      if hasMath then
        meta['asciidoc-stem'] = 'latexmath'
      end 

      -- We construct the title with cross ref information into the metadata
      -- if we see such a title, we need to move the identifier up outside the title
      local titleInlines = meta['title']
      if #titleInlines == 1 and titleInlines[1].t == 'Span' then ---@diagnostic disable-line
        
        ---@type pandoc.Span
        local span = titleInlines[1]
        local identifier = span.identifier
        
        -- if there is an identifier in the title, we should take over and emit
        -- the proper asciidoc
        if identifier ~= nil then
          -- this is a chapter title, tear out the id and make it ourselves
          local titleContents = pandoc.write(pandoc.Pandoc({span.content}), "asciidoc")
          meta['title'] = pandoc.RawInline("asciidoc", titleContents)
          meta['title-prefix'] = pandoc.RawInline("asciidoc", "[[" .. identifier .. "]]")
        end
      end

      return meta
    end,
    Math = function(el)
      hasMath = true;
    end,
    Cite = function(el) 
      -- If quarto is going to be processing the cites, go ahead and convert
      -- them to a native cite
      if param(kAsciidocNativeCites) then
        local citesStr = table.concat(el.citations:map(function (cite) 
          return '<<' .. cite.id .. '>>'
        end))
        return pandoc.RawInline("asciidoc", citesStr);
      end
    end,
    Callout = function(el) 
      -- callout -> admonition types pass through
      local admonitionType = el.type:upper();

      -- render the callout contents
      local admonitionContents = pandoc.write(pandoc.Pandoc(el.content), "asciidoc")

      local admonitionStr;
      if el.title then
        -- A titled admonition
        local admonitionTitle = pandoc.write(pandoc.Pandoc({el.title}), "asciidoc")
        admonitionStr = "[" .. admonitionType .. "]\n." .. admonitionTitle .. "====\n" .. admonitionContents .. "====\n\n" 
      else
        -- A titleless admonition
          admonitionStr = "[" .. admonitionType .. "]\n====\n" .. admonitionContents .. "====\n\n" 
      end
      return pandoc.RawBlock("asciidoc", admonitionStr)
    end,
    Inlines = function(el)
      -- Walk inlines and see if there is an inline code followed directly by a note. 
      -- If there is, place a space there (because otherwise asciidoctor may be very confused)
      for i, v in ipairs(el) do

        if v.t == "Code" then
          if el[i+1] and el[i+1].t == "Note" then

            local noteEl = el[i+1]
            -- if the note contains a code inline, we need to add a space
            local hasCode = false
            pandoc.walk_inline(noteEl, {
              Code = function(_el)
                hasCode = true
              end
            })

            -- insert a space
            if hasCode then
              table.insert(el, i+1, pandoc.RawInline("asciidoc", "{empty}"))
            end
          end
        end
        
      end
      return el

    end
  }
end


-- book.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

--- Removes notes and links
local function clean (inlines)
  -- this is in post, so it's after render, so we don't need to worry about
  -- custom ast nodes
  return inlines:walk {
    Note = function (_) return {} end,
    Link = function (link) return link.content end,
  }
end

--- Creates an Inlines singleton containing the raw LaTeX.
local function l(text)
  return pandoc.Inlines{pandoc.RawInline('latex', text)}
end

-- inject metadata
function quartoBook()
  return {
    Header = function(el) 
      if (quarto.doc.is_format("pdf") and param("single-file-book", false)) then
          -- Works around https://github.com/jgm/pandoc/issues/1632
          -- See https://github.com/quarto-dev/quarto-cli/issues/2412
          if el.level <= 2 and el.classes:includes 'unnumbered' then
            local title = clean(el.content)
            local secmark = el.level == 1
              and l'\\markboth{' .. title .. l'}{' .. title .. l'}'
              or l'\\markright{' .. title .. l'}' -- subsection, keep left mark unchanged
            return {el, secmark}
          end
      end
    end,
    CodeBlock = function(el)

      -- If this is a title block cell, we should render it
      -- using the template
      if el.attr.classes:includes('quarto-title-block') then

        -- read the contents of the code cell
        -- this should just be some metadata 
        local renderedDoc = pandoc.read(el.text, 'markdown')

        -- render the title block using the metdata and
        -- and the template
        local template = el.attr.attributes['template']

        -- process any author information
        local processedMeta = processAuthorMeta(renderedDoc.meta)

        -- read the title block template
        local renderedBlocks = compileTemplate(template, processedMeta)

        if #renderedBlocks ~= 0 then
          local emptyLine = pandoc.LineBreak()
          renderedBlocks:insert(emptyLine)
        end 

        return renderedBlocks
      end
    end
  }
end

-- cites.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

local discoveredCites = pandoc.List() 
local kRefsIndentifier = "refs-target-identifier"

function indexCites()   
  return {
    Div = function(el) 
      local refsIndentifier = param(kRefsIndentifier)
      if el.attr.identifier == 'refs' and refsIndentifier then 
        tappend(el.content, {pandoc.Plain(refsIndentifier)})
        return el;
      end
    end,
    Cite = function(el) 
      for i,v in ipairs(el.citations) do
        discoveredCites:insert(v.id)
      end
    end
  }
end

function writeCites() 
  return {
    Pandoc = function(el)
      -- the file to write to
      local citesFilePath = param("cites-index-file")
      if citesFilePath and quarto.project.directory then
        -- open the file
        local citesRaw = _quarto.file.read(citesFilePath)
        local documentCites = {}
        if citesRaw then
          documentCites = quarto.json.decode(citesRaw)
        end

        -- write the cites
        local inputFile = quarto.doc.input_file
        local relativeFilePath = pandoc.path.make_relative(inputFile, quarto.project.directory)
        documentCites[relativeFilePath] = discoveredCites

        -- write the file
        local json = quarto.json.encode(documentCites)
        local file = io.open(citesFilePath, "w")
        if file ~= nil then
          file:write(json .. "\n")
          file:close()
        else
          fail('Error opening book citations file at ' .. citesFilePath)
        end
      end
    end
  }
end
-- delink.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

local kDelinkClass = 'delink'

function delink() 
  return {
    -- Removes links from any divs marked with 'delink' class
    Div = function(div)
      if _quarto.format.isHtmlOutput() and div.attr.classes:includes(kDelinkClass) then

        -- remove the delink class 
        for i, clz in ipairs(div.attr.classes) do 
          if clz == kDelinkClass then
            div.attr.classes:remove(i)
          end
        end

        -- find links and transform them to spans
        -- this is in post, so it's after render, so we don't need to worry about
        -- custom ast nodes
        return pandoc.walk_block(div, {
          Link = function(link)
            return pandoc.Span(link.content)
          end
        })
      end
    end
  }
end
-- fig-cleanup.lua
-- Copyright (C) 2021-2022 Posit Software, PBC


local function stripFigAnonymous(el)
  if isAnonymousFigId(el.attr.identifier) then
    el.attr.identifier = ""
    return el
  end
end

function figCleanup() 
  return {
    Div = stripFigAnonymous,
    Image = stripFigAnonymous
  }
end


-- foldcode.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function foldCode()
  return {
    CodeBlock = function(block)
      if _quarto.format.isHtmlOutput() or _quarto.format.isMarkdownWithHtmlOutput() then
        if block.attr.classes:includes("cell-code") then
          local fold = foldAttribute(block)
          local summary = summaryAttribute(block)
          if fold ~= nil or summary ~= nil then
            block.attr.attributes["code-fold"] = nil
            block.attr.attributes["code-summary"] = nil
            if fold ~= "none" then 
              local blocks = pandoc.List()
              postState.codeFoldingCss =  _quarto.format.isHtmlOutput()
              local open = ""
              if fold == "show" then
                open = " open"
              end
              local style = ""
              if block.attr.classes:includes("hidden") then
                style = ' class="hidden"'
              end
              local beginPara = pandoc.Plain({
                pandoc.RawInline("html", "<details" .. open .. style .. ">\n<summary>"),
              })
              
              if not isEmpty(summary) then
                tappend(beginPara.content, markdownToInlines(summary))
              end
              beginPara.content:insert(pandoc.RawInline("html", "</summary>"))
              blocks:insert(beginPara)
              blocks:insert(block)
              blocks:insert(pandoc.RawBlock("html", "</details>"))
              return blocks
            else
              return block
            end
          end
        end
      end
    end
  }
end

function isEmpty(str) 
  return str == nil or string.len(trim(str)) == 0
end

function foldAttribute(el)
  local default = param("code-fold")
  if default then
    default = pandoc.utils.stringify(default)
  else
    default = "none"
  end
  local fold = attribute(el, "code-fold", default)
  if fold == true or fold == "true" or fold == "1" then
    return "hide"
  elseif fold == nil or fold == false or fold == "false" or fold == "0" then
    return "none"
  else
    return tostring(fold)
  end
end

function summaryAttribute(el)
  local default = param("code-summary")
  if default then
    default = pandoc.utils.stringify(default)
  else
    default = "Code"
  end
  return attribute(el, "code-summary", default)
end


-- ipynb.lua
-- Copyright (C) 2021-2022 Posit Software, PBC


function ipynb()
  if FORMAT == "ipynb" then
    return {

      Pandoc = function(doc)

        -- pandoc doesn'tx handle front matter title/author/date when creating ipynb
        -- so do that manually here. note that when we make authors more 
        -- sophisticated we'll need to update this code

        -- read the title block template
        local titleBlockTemplate = param('ipynb-title-block')

        -- render the title block template
        local renderedBlocks = compileTemplate(titleBlockTemplate, doc.meta)

        -- prepend the blocks to the notebook
        tprepend(doc.blocks, renderedBlocks)

        return doc
        
      end,

      Div = function(el)
        if el.attr.classes:includes('cell') then
          el.attr.classes:insert('code')
        end
        el.attr.classes = fixupCellOutputClasses(
          el.attr.classes, 
          'cell-output-stdout', 
          { 'stream', 'stdout' }
        )
        el.attr.classes = fixupCellOutputClasses(
          el.attr.classes, 
          'cell-output-stderr', 
          { 'stream', 'stderr' }
        )
        el.attr.classes = fixupCellOutputClasses(
          el.attr.classes, 
          'cell-output-display', 
          { 'display_data' }
        )
        el.attr.classes = removeClass(el.attr.classes, 'cell-output')
        return el
      end,
    
      CodeBlock = function(el)
        if (el.attr.classes:includes('cell-code')) then
          el.attr.classes = removeClass(el.attr.classes, 'cell-code')
        end
      end,

      -- remove image classes/attributes (as this causes Pandoc to write raw html, which in turn
      -- prevents correct handling of attachments in some environments including VS Code)
      Image = function(el)
        el.attr = pandoc.Attr()
        return el
      end,

      -- note that this also catches raw blocks inside display_data 
      -- but pandoc seems to ignore the .cell .raw envelope in this
      -- case and correctly produce text/html cell output
      RawBlock = function(el)
        local rawDiv = pandoc.Div(
          { el }, 
          pandoc.Attr("", { "cell", "raw" })
        )
        return rawDiv
      end
    }
  else
    return {}
  end
end

function fixupCellOutputClasses(classes, cellOutputClass, outputClasses)
  if classes:includes(cellOutputClass) then
    classes = removeClass(classes, cellOutputClass)
    classes:insert("output")
    tappend(classes, outputClasses)
  end
  return classes
end

function readMetadataInlines(meta, key)
  val = meta[key]
  if type(val) == "boolean" then
    return { pandoc.Str( tostring(val) ) } 
  elseif type(val) == "string" then
    return stringToInlines(val)     
  elseif pandoc.utils.type(val) == "Inlines" then
    return val
  else
   return nil
  end
end

--[[
     A Pandoc 2 Lua filter converting Pandoc native divs to LaTeX environments
     Author: Romain Lesur, Christophe Dervieux, and Yihui Xie
     License: Public domain
     Ported from: https://github.com/rstudio/rmarkdown/blob/80f14b2c6e63dcb8463df526354f4cd4fc72fd04/inst/rmarkdown/lua/latex-div.lua
--]]

function latexDiv()
  return {
    Div = function (divEl)
      -- look for 'latex' or 'data-latex' and at least 1 class
      local options = attribute(divEl, 'latex', attribute(divEl, 'data-latex'))
      if not options or #divEl.attr.classes == 0 then
        return nil
      end
      
      -- if the output format is not latex, remove the attr and return
      if not _quarto.format.isLatexOutput() then
        divEl.attributes['latex'] = nil
        divEl.attributes['data-latex'] = nil
        return divEl
      end
      
      -- if it's "1" or "true" then just set it to empty string
      if options == "1" or pandoc.text.lower(options) == "true" then
        options = ""
      end
    
      -- environment begin/end
      local env = divEl.classes[1]
      local beginEnv = '\\begin' .. '{' .. env .. '}' .. options
      local endEnv = '\n\\end{' .. env .. '}'
      
      -- if the first and last div blocks are paragraphs then we can
      -- bring the environment begin/end closer to the content
      if divEl.content[1].t == "Para" and divEl.content[#divEl.content].t == "Para" then
        table.insert(divEl.content[1].content, 1, pandoc.RawInline('tex', beginEnv .. "\n"))
        table.insert(divEl.content[#divEl.content].content, pandoc.RawInline('tex', "\n" .. endEnv))
      else
        table.insert(divEl.content, 1, pandoc.RawBlock('tex', beginEnv))
        table.insert(divEl.content, pandoc.RawBlock('tex', endEnv))
      end
      return divEl
    end
  }

end
-- meta.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- inject metadata
function quartoPostMetaInject()
  return {
    Meta = function(meta)
      metaInjectLatex(meta, function(inject)
        if postState.usingTikz then
          inject(usePackage("tikz"))
        end
      end)
    
      -- don't emit unnecessary metadata
      meta["quarto-filters"] = nil

      return meta
    end
  }
end

-- ojs.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function ojs()

  local uid = 0
  local cells = pandoc.List()

  local function uniqueId()
    uid = uid + 1
    return "ojs-element-id-" .. uid
  end

  local function ojsInline(src)
    local id = uniqueId()
    cells:insert({
        src = src,
        id = id,
        inline = true
    })
    return pandoc.Span('', { id = id })
  end

  local function isInterpolationOpen(str)
    if str.t ~= "Str" then
      return false
    end
    return str.text:find("${")
  end

  local function isInterpolationClose(str)
    if str.t ~= "Str" then
      return false
    end
    return str.text:find("}")
  end

  local function findArgIf(lst, fun, start)
    if start == nil then
      start = 1
    end
    local sz = #lst
    for i=start, sz do
      if fun(lst[i]) then
        return i
      end
    end
    return nil
  end

  local function escapeSingle(str)
    local sub, _ = string.gsub(str, "'", "\\\\'")
    return sub
  end

  local function escapeDouble(str)
    local sub, _ = string.gsub(str, '"', '\\\\"')
    return sub
  end

  local function stringifyTokenInto(token, sequence)
    local function unknown()
      fail("Don't know how to handle token " .. token.t)
    end
    if     token.t == 'Cite' then
      unknown()
    elseif token.t == 'Code' then
      sequence:insert('`')
      sequence:insert(token.text)
      sequence:insert('`')
    elseif token.t == 'Emph' then
      sequence:insert('*')
      sequence:insert(token.text)
      sequence:insert('*')
    elseif token.t == 'Image' then
      unknown()
    elseif token.t == 'LineBreak' then
      sequence:insert("\n")
    elseif token.t == 'Link' then
      unknown()
    elseif token.t == 'Math' then
      unknown()
    elseif token.t == 'Note' then
      unknown()
    elseif token.t == 'Quoted' then
      if token.quotetype == 'SingleQuote' then
        sequence:insert("'")
        local innerContent = stringifyTokens(token.content)
        sequence:insert(escapeSingle(innerContent))
        sequence:insert("'")
      else
        sequence:insert('"')
        local innerContent = stringifyTokens(token.content)
        sequence:insert(escapeDouble(innerContent))
        sequence:insert('"')
      end
    elseif token.t == 'RawInline' then
      sequence:insert(token.text)
    elseif token.t == 'SmallCaps' then
      unknown()
    elseif token.t == 'SoftBreak' then
      sequence:insert("\n")
    elseif token.t == 'Space' then
      sequence:insert(" ")
    elseif token.t == 'Span' then
      stringifyTokenInto(token.content, sequence)
    elseif token.t == 'Str' then
      sequence:insert(token.text)
    elseif token.t == 'Strikeout' then
      unknown()
    elseif token.t == 'Strong' then
      sequence:insert('**')
      sequence:insert(token.text)
      sequence:insert('**')
    elseif token.t == 'Superscript' then
      unknown()
    elseif token.t == 'Underline' then
      sequence:insert('_')
      sequence:insert(token.text)
      sequence:insert('_')
    else
      unknown()
    end
  end
  
  local function stringifyTokens(sequence)
    local result = pandoc.List()
    for i = 1, #sequence do
      stringifyTokenInto(sequence[i], result)
    end
    return table.concat(result, "")
  end

  local function escape_quotes(str)
    local sub, _ = string.gsub(str, '\\', '\\\\')
    sub, _ = string.gsub(sub, '"', '\\"')
    sub, _ = string.gsub(sub, "'", "\\'")
    sub, _ = string.gsub(sub, '`', '\\\\`')
    return sub
  end
  
  local function inlines_rec(inlines)
    -- FIXME I haven't tested this for nested interpolations
    local i = findArgIf(inlines, isInterpolationOpen)
    while i do
      if i then
        local j = findArgIf(inlines, isInterpolationClose, i)
        if j then
          local is, ie = inlines[i].text:find("${")
          local js, je = inlines[j].text:find("}")
          local beforeFirst = inlines[i].text:sub(1, is - 1)
          local firstChunk = inlines[i].text:sub(ie + 1, -1)
          local lastChunk = inlines[j].text:sub(1, js - 1)
          local afterLast = inlines[j].text:sub(je + 1, -1)

          local slice = {pandoc.Str(firstChunk)}
          local slice_i = 2
          for k=i+1, j-1 do
            slice[slice_i] = inlines[i+1]
            slice_i = slice_i + 1
            inlines:remove(i+1)
          end
          slice[slice_i] = pandoc.Str(lastChunk)
          inlines:remove(i+1)
          inlines[i] = pandoc.Span({
              pandoc.Str(beforeFirst),
              ojsInline(stringifyTokens(slice)),
              pandoc.Str(afterLast)
          })
        end
        -- recurse
        i = findArgIf(inlines, isInterpolationOpen, i+1)
      end
    end
    return inlines
  end  

  if (param("ojs", false)) then
    return {
      Inlines = function (inlines)
        return inlines_rec(inlines)
      end,
      
      Pandoc = function(doc)
        if uid > 0 then
          doc.blocks:insert(pandoc.RawBlock("html", "<script type='ojs-module-contents'>"))
          doc.blocks:insert(pandoc.RawBlock("html", '{"contents":['))
          for i, v in ipairs(cells) do
            local inlineStr = ''
            if v.inline then
              inlineStr = 'true'
            else
              inlineStr = 'false'
            end
            if i > 1 then
              doc.blocks:insert(",")
            end
            doc.blocks:insert(
              pandoc.RawBlock(
                "html",
                ('  {"methodName":"interpret","inline":"true","source":"htl.html`<span>${' ..
                 escape_quotes(v.src) .. '}</span>`", "cellName":"' .. v.id .. '"}')))
          end
          doc.blocks:insert(pandoc.RawBlock("html", ']}'))
          doc.blocks:insert(pandoc.RawBlock("html", "</script>"))
        end
        return doc
      end,
      
      Str = function(el)
        local b, e, s = el.text:find("${(.+)}")
        if s then
          return pandoc.Span({
              pandoc.Str(string.sub(el.text, 1, b - 1)),
              ojsInline(s),
              pandoc.Str(string.sub(el.text, e + 1, -1))
          })
        end
      end
    }
  else 
    return {}
  end

end
-- jats.lua
-- Copyright (C) 2021-2022 Posit Software, PBC




function jats()
  return {
    -- clear out divs
    Div = function(div)
      if _quarto.format.isJatsOutput() then
        -- unroll blocks contained in divs
        local blocks = pandoc.List()
        for _, childBlock in ipairs(div.content) do
          if childBlock.t == "Div" then
            tappend(blocks, childBlock.content)
          else
            blocks:insert(childBlock)
          end
        end
        return blocks
      end
    end
  }
end
-- responsive.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function responsive() 
  return {
    -- make images responsive (unless they have an explicit height attribute)
    Image = function(image)
      if _quarto.format.isHtmlOutput() and param('fig-responsive', false) then
        if not image.attr.attributes["height"] and not image.attr.attributes["data-no-responsive"] then
          image.attr.classes:insert("img-fluid")
          return image
        end
      end
    end
  }
end

function responsive_table()
  return {
    -- make simple HTML tables responsive (if they contain a .responsive(-*) class)
    Table = function(tbl)

      if _quarto.format.isHtmlOutput() == false then
        return tbl
      end

      local table_responsive_nm = {
        ["responsive"    ]       = "table-responsive"    ,
        ["responsive-sm" ]       = "table-responsive-sm" ,
        ["responsive-md" ]       = "table-responsive-md" ,
        ["responsive-lg" ]       = "table-responsive-lg" ,
        ["responsive-xl" ]       = "table-responsive-xl" ,
        ["responsive-xxl"]       = "table-responsive-xxl",
        ["table-responsive"    ] = "table-responsive"    ,
        ["table-responsive-sm" ] = "table-responsive-sm" ,
        ["table-responsive-md" ] = "table-responsive-md" ,
        ["table-responsive-lg" ] = "table-responsive-lg" ,
        ["table-responsive-xl" ] = "table-responsive-xl" ,
        ["table-responsive-xxl"] = "table-responsive-xxl"
      }

      local found, found_key
      for _, v in ipairs(tbl.classes) do
        if table_responsive_nm[v] then
          found = table_responsive_nm[v]
          found_key = v
          break
        end
      end
      if not found then
        return tbl
      end

      tbl.classes = tbl.classes:filter(function(class) 
        return class ~= found_key 
      end)
        
      return pandoc.Div(tbl, pandoc.Attr("", { found }))
    end
  }
end
-- reveal.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

local kShowNotes = 'showNotes'

function reveal()
  if _quarto.format.isRevealJsOutput() then
    return combineFilters{
      {
        Meta = function(meta)           
          if meta[kShowNotes] ~= nil and pandoc.utils.type(meta[kShowNotes]) == "Inlines" then
            meta[kShowNotes]:insert(1, '"')
            meta[kShowNotes]:insert('"')
            return meta
          end
        end,
        Div = applyPosition,
        Span = applyPosition,
        Image = applyPosition
      },
      {
        Div = fencedDivFix
      }
    }
  else
    return {}
  end
end

function applyPosition(el)
  if el.attr.classes:includes("absolute") then
    -- translate position attributes into style
    local style = el.attr.attributes['style']
    if style == nil then
      style = ''
    end
    local attrs = pandoc.List({ "top", "left", "bottom", "right", "width", "height" })
    for _, attr in ipairs(attrs) do
      local value = el.attr.attributes[attr]
      if value ~= nil then
        style = style .. attr .. ': ' .. asCssSize(value) .. '; '
        el.attr.attributes[attr] = nil
      end
    end
    el.attr.attributes['style'] = style
    return el
  end
end

function asCssSize(size)
  local number = tonumber(size)
  if number ~= nil then
    return tostring(number) .. "px"
  else
    return size
  end
end

function fencedDivFix(el)
  -- to solve https://github.com/quarto-dev/quarto-cli/issues/976
  -- until Pandoc may deal with it https://github.com/jgm/pandoc/issues/8098
  if el.content[1] and el.content[1].t == "Header" and el.attr.classes:includes("fragment") then
    level = PANDOC_WRITER_OPTIONS.slide_level
    if level and el.content[1].level > level then
      -- This will prevent Pandoc to create a <section>
      el.content:insert(1, pandoc.RawBlock("html", "<!-- -->"))
    end
  end
  return el
end
-- tikz.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function tikz()
  if _quarto.format.isLatexOutput() then
    return {
      Image = function(image)
        if latexIsTikzImage(image) then
          return latexFigureInline(image, postState)
        end
      end
    }
  else
    return {}
  end
end
-- svg.lua
-- Copyright (C) 2021 by RStudio, PBC

local function convert_svg(path)
  local stem = pandoc.path.split_extension(path)
  local output = stem .. '.pdf'

  local status, results = pcall(pandoc.pipe, "rsvg-convert", {"-f", "pdf", "-a", "-o", output, path}, "")
  if status then
    return output
  else 
    if results['command'] == nil then
      -- command not found
      error("Failed when attempting to convert a SVG to a PDF for output. Please ensure that rsvg-convert is available on the path.")
      os.exit(1)
    else
      error("Failed when attempting to convert a SVG to a PDF for output. An error occurred while attempting to run rsvg-convert.\nError code " .. tostring(results['error_code']) )
      os.exit(1)
    end
  end
end

local mimeImgExts = {
  ["image/jpeg"]="jpg",
  ["image/gif"]="gif",
  ["image/vnd.microsoft.icon"]="ico",
  ["image/avif"]="avif",
  ["image/bmp"]="bmp",
  ["image/png"]="png",
  ["image/svg+xml"]="svg",
  ["image/tiff"]="tif",
  ["image/webp"]="webp",
}


-- A cache of image urls that we've resolved into the mediabag
-- keyed by {url: mediabagpath}
local resolvedUrls = {}

-- windows has a max path length of 260 characters
-- but we'll be conservative since we're sometimes appending a number
local windows_safe_filename = function(filename)
  -- pull the first 200 characters without the extension
  local stem, ext = pandoc.path.split_extension(filename)
  local safeStem = stem:sub(1, 20)

  local result = safeStem .. ext

  if #ext > 40 then
    -- if the extension is too long, truncate it
    result = safeStem .. ext:sub(1, 40)
  end
  return result
end

-- replace invalid tex characters with underscores
local tex_safe_filename = function(filename)
  -- return filename
  return filename:gsub("[ <>()|:&;#?*'\\/]", '-')
end

function pdfImages() 
  return {
    -- convert SVG images to PDF when rendering PDFS
    Image = function(image)
      if quarto.doc.is_format("pdf") then
        if _quarto.file.exists(image.src) then
          -- If the src is pointing to a local file that is an svg, process it
          local ext = select(2, pandoc.path.split_extension(image.src))
          if ext == '.svg' then
            local convertedPath = convert_svg(image.src)
            if convertedPath then
              local contents = _quarto.file.read(convertedPath)
              local relativePath = pandoc.path.make_relative(convertedPath, '.')

              -- add to media bag and remove the converted file
              pandoc.mediabag.insert(relativePath, 'application/pdf', contents)
              _quarto.file.remove(relativePath)
              
              image.src = relativePath
              return image
            end
          end
        else
          -- See if the path points to an SVG in the media bag
          -- (been generated by a filter, for example)
          local mt, contents = pandoc.mediabag.lookup(image.src)
          if mt == 'image/svg+xml' then
            local result = pandoc.system.with_temporary_directory('svg-convert', function (tmpdir) 

              -- write the media bag contents to a temp file
              local filename = image.src
              local tempPath = pandoc.path.join({tmpdir, filename})
              local file = _quarto.file.write(tempPath, contents)
              
              if file then
                -- convert to svg
                local convertedPath = convert_svg(tempPath)
                if convertedPath then
                  -- compute the correct relative path to the newly created file
                  local mbPath = pandoc.path.make_relative(convertedPath, tmpdir, false)
                  local mbContents = _quarto.file.read(convertedPath)
                  
                  -- place the new file in the mediabag, remove the old
                  pandoc.mediabag.insert(mbPath, 'application/pdf', mbContents)
                  pandoc.mediabag.delete(filename)

                  -- update the path
                  image.src = mbPath
                  return image
                end
              end
              return nil
            end)
            return result
          elseif mt == nil then
            -- This file doesn't exist and isn't in the media bag
            -- see if it need to be fetched
            if resolvedUrls[image.src] then
              image.src = resolvedUrls[image.src]
              return image
            else 
              local relativePath = image.src:match('https?://[%w%.%:]+/(.+)')
              if relativePath then

                local imgMt, imgContents = pandoc.mediabag.fetch(image.src)
                local decodedSrc = fullyUrlDecode(image.src)                
                if decodedSrc == nil then
                  decodedSrc = "unknown"
                end

                local function filenameFromMimeType(filename, imgMt)
                  -- Use the mime type to compute an extension when possible
                  -- This will allow pandoc to properly know the type, even when 
                  -- the path to the image is a difficult to parse URI
                  local mimeExt = mimeImgExts[imgMt]
                  if mimeExt then
                    local stem, _ext = pandoc.path.split_extension(filename)
                    return stem .. '.' .. mimeExt
                  else
                    return filename
                  end
                end

                -- compute the filename for this file
                local basefilename = pandoc.path.filename(decodedSrc)
                local safefilename = windows_safe_filename(tex_safe_filename(basefilename))
                local filename = filenameFromMimeType(safefilename, imgMt)

                if imgMt ~= nil then
                  local existingMt = pandoc.mediabag.lookup(filename)
                  local counter = 1
                  while (existingMt) do
                    local stem, ext = pandoc.path.split_extension(filename)
                    filename = stem .. counter .. ext
                    existingMt = pandoc.mediabag.lookup(filename)
                    counter = counter + 1
                  end
                  resolvedUrls[image.src] = filename
                  pandoc.mediabag.insert(filename, imgMt, imgContents)
                  image.src = filename
                  return image
                end
              end
            end
          end
        end
      end
    end
  }
end


-- cellcleanup.lua
-- Copyright (C) 2020-2023 Posit Software, PBC

function cell_cleanup()
  return {
    Div = function(div)
      if (#div.classes == 1 and 
          div.classes[1] == "cell" and
          #div.content == 0) then
        return {}
      end
    end
  }
end
-- bibliography.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function bibliography() 
  return {
    Div = function(el)
      local citeMethod = param('cite-method', 'citeproc')
      if _quarto.format.isLatexOutput() and el.attr.identifier == "refs" and citeMethod ~= 'citeproc' then
        return pandoc.RawBlock("latex", '%bib-loc-124C8010')
      end
    end
  }
end
-- dependencies.lua
-- Copyright (C) 2020-2022 Posit Software, PBC



function dependencies()
  return {
    Meta = function(meta) 
      -- Process the final dependencies into metadata
      -- and the file responses
      _quarto.processDependencies(meta)
      return meta
    end
  }
end
-- book-cleanup.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function bookCleanup() 
  if (param("single-file-book", false)) then
    return {
      RawInline = cleanupFileMetadata,
      RawBlock = cleanupFileMetadata,
      Div = cleanupBookPart,
      Para = cleanupEmptyParas
    }
  else
    return {
      RawInline = cleanupFileMetadata,
      RawBlock = cleanupFileMetadata,
      Para = cleanupEmptyParas
    }
  end
end

function cleanupEmptyParas(el)
  if not next(el.content) then
    return {}
  end  
end

function cleanupFileMetadata(el)
  if _quarto.format.isRawHtml(el) then
    local rawMetadata = string.match(el.text, "^<!%-%- quarto%-file%-metadata: ([^ ]+) %-%->$")
    if rawMetadata then
      return {}
    end
  end
  return el
end

function cleanupBookPart(el)
  if el.attr.classes:includes('quarto-book-part') and not _quarto.format.isLatexOutput() then
    return pandoc.Div({})
  end
end

-- quarto-finalize.lua
-- Copyright (C) 2022 Posit Software, PBC

function mediabag()
  return {
    -- mediabag entries need to be re-routed to the filesystem
    -- if this isn't an office doc (as those formats automatically
    -- scoop up mediabag files)
    Image = function(el)
      if not _quarto.format.isWordProcessorOutput() and
         not _quarto.format.isPowerPointOutput() then
        local mt, contents = pandoc.mediabag.lookup(el.src)
        if contents ~= nil then
          
          local mediabagDir = param("mediabag-dir", nil)
          local mediaFile = pandoc.path.join{mediabagDir, el.src}

          local file = _quarto.file.write(mediaFile, contents)
          if not file then
            warn('failed to write mediabag entry: ' .. mediaFile)
          end
          el.src = mediaFile
          return el
        end
      end
    end
  }
end
-- meta-cleanup.lua
-- Copyright (C) 2022 Posit Software, PBC

function metaCleanup()
  return {
    Meta = function(meta)
      if _quarto.format.isAstOutput() then
        removeAllEmptyIncludes(meta)
        return meta
      end
    end
  }
end
-- authors.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- required version
PANDOC_VERSION:must_be_at_least '2.13'

-- global state
authorsState = {}

-- [import]
function import(script)
  local path = PANDOC_SCRIPT_FILE:match("(.*[/\\])")
  dofile(path .. script)
end

-- [/import]

function normalizeFilter() 
  return {
    Meta = function(meta)
      -- normalizes the author/affiliation metadata
      local normalized = processAuthorMeta(meta)

      -- normalizes the citation metadata
      normalized = processCitationMeta(normalized)

      -- normalizes the license metadata
      normalized = processLicenseMeta(normalized)

      -- for JATs, forward keywords or categories to tags
      if _quarto.format.isJatsOutput() then
        if normalized.tags == nil then
          if normalized.keywords ~= nil then
            normalized.tags = normalized.keywords
          elseif meta.categories ~= nil then
            normalized.tags = normalized.categories
          end
        end
      end

      return normalized
    end
  }
end

-- parsehtml.lua
-- Copyright (C) 2020-2023 Posit Software, PBC

local kDisableProcessing = "quarto-disable-processing"

local function preprocess_table_text(src)
  -- html manipulation with regex is fraught, but those specific
  -- changes are safe assuming that no one is using quarto- as
  -- a prefix for dataset attributes in the tables.
  -- See
  -- * https://www.w3.org/html/wg/spec/syntax.html#start-tags
  -- * https://www.w3.org/html/wg/spec/syntax.html#end-tags

  src = src:gsub("<th([%s>])", "<td data-quarto-table-cell-role=\"th\"%1")
  src = src:gsub("</th([%s>])", "</td%1")
  src = src:gsub("<table([%s>])", "<table data-quarto-postprocess=\"true\"%1")

  return src
end

function parse_html_tables()
  local filter
  filter = {
    RawBlock = function(el)
      if _quarto.format.isRawHtml(el) then
        -- if we have a raw html table in a format that doesn't handle raw_html
        -- then have pandoc parse the table into a proper AST table block
        local pat = htmlTablePattern()
        local i, j = string.find(el.text, pat)
        if i == nil then
          return nil
        end

        local tableBegin,tableBody,tableEnd = el.text:match(pat)
        if tableBegin then
          local before_table = string.sub(el.text, 1, i - 1)
          local after_table = string.sub(el.text, j + 1)
          local tableHtml = tableBegin .. "\n" .. tableBody .. "\n" .. tableEnd
          -- Pandoc's HTML-table -> AST-table processing does not faithfully respect
          -- `th` vs `td` elements. This causes some complex tables to be parsed incorrectly,
          -- and changes which elements are `th` and which are `td`.
          --
          -- For quarto, this change is not acceptable because `td` and `th` have
          -- accessibility impacts (see https://github.com/rstudio/gt/issues/678 for a concrete
          -- request from a screen-reader user).
          --
          -- To preserve td and th, we replace `th` elements in the input with 
          -- `td data-quarto-table-cell-role="th"`. 
          -- 
          -- Then, in our HTML postprocessor,
          -- we replace th elements with td (since pandoc chooses to set some of its table
          -- elements as th, even if the original table requested not to), and replace those 
          -- annotated td elements with th elements.

          tableHtml = preprocess_table_text(tableHtml)
          local tableDoc = pandoc.read(tableHtml, "html")
          local skip = false
          local found = false
          _quarto.ast.walk(tableDoc, {
            Table = function(table)
              found = true
              if table.attributes[kDisableProcessing] ~= nil then
                skip = true
              end
            end
          })
          if not found then
            warn("Unable to parse table from raw html block: skipping.")
            return nil
          end
          if skip then
            return nil
          end
          local blocks = pandoc.Blocks({})
          if before_table ~= "" then
            -- this clause is presently redundant, but if we ever
            -- parse more than one type of element, then we'll
            -- need it. We keep it here for symmetry with
            -- the after_table clause.
            local block = pandoc.RawBlock(el.format, before_table)
            local result = _quarto.ast.walk(block, filter)
            if type(result) == "table" then
              blocks:extend(result)
            else
              blocks:insert(result)
            end
          end
          blocks:extend(tableDoc.blocks)
          if after_table ~= "" then
            local block = pandoc.RawBlock(el.format, after_table)
            local result = _quarto.ast.walk(block, filter)
            if type(result) == "table" then
              blocks:extend(result)
            else
              blocks:insert(result)
            end
          end
          return blocks
        end
      end
      return el
    end
  }
  return filter
end
-- pandoc3.lua
-- Copyright (C) 2020-2023 Posit Software, PBC

function parse_pandoc3_figures() 
  local walk_recurse
  walk_recurse = function(constructor)
    local plain_figure_treatment = function(el)
      return _quarto.ast.walk(el, walk_recurse(pandoc.Plain))
    end
    local para_figure_treatment = function(el)
      return _quarto.ast.walk(el, walk_recurse(pandoc.Para))
    end
    return {
      traverse = "topdown",
      BulletList = para_figure_treatment,
      BlockQuote = plain_figure_treatment,
      Table = plain_figure_treatment,
      Div = para_figure_treatment,
      OrderedList = plain_figure_treatment,
      Note = plain_figure_treatment,
      Figure = function(fig)
        if (#fig.content == 1 and fig.content[1].t == "Plain") then
          local forwarded_id = false
          return constructor(_quarto.ast.walk(fig.content[1].content, {
            Image = function(image)
              image.classes:extend(fig.classes)
              for k, v in pairs(fig.attributes) do
                image.attributes[k] = v
              end
              if fig.identifier ~= "" then
                if not forwarded_id then
                  image.identifier = fig.identifier
                  forwarded_id = true
                end
              end
              return image
            end
          }))
        else
          error("Couldn't parse figure:")
          error(fig)
          crash_with_stack_trace()
        end
      end
    }
  end

  return {
    Pandoc = function(doc)
      local result = _quarto.ast.walk(doc, walk_recurse(pandoc.Para))
      return result
    end
  }
end

function render_pandoc3_figures() 
  -- only do this in jats because other formats emit <figure> inadvertently otherwise
  -- with potentially bad captions.
  -- 
  -- this will change with new crossref system anyway.
  if not _quarto.format.isJatsOutput() then
    return {}
  end
  
  return {
    Para = function(para)
      if (#para.content == 1 and para.content[1].t == "Image" and
          hasFigureRef(para.content[1])) then
        
        -- the image
        local img = para.content[1]
        
        -- clear the id (otherwise the id will be present on both the image)
        -- and the figure
        local figAttr = img.attr:clone()
        img.attr.identifier = ""
        
        local caption = img.caption
        return pandoc.Figure(
          pandoc.Plain(para.content[1]),
          {
            short = nil,
            long = {pandoc.Plain(caption)}
          },
          figAttr)
      end
    end,
  }
end
local function process_quarto_markdown_input_element(el)
  if el.attributes.qmd ~= nil then
    return pandoc.read(el.attributes.qmd, "markdown")
  elseif el.attributes["qmd-base64"] ~= nil then
    return pandoc.read(quarto.base64.decode(el.attributes["qmd-base64"]), "markdown")
  else
    error("process_quarto_markdown_input_element called with element that does not have qmd or qmd-base64 attribute")
  end
end

function extract_quarto_dom()
  return {
    Div = function(div)
      if div.attributes.qmd ~= nil or div.attributes["qmd-base64"] ~= nil then
        local doc = process_quarto_markdown_input_element(div)
        return doc.blocks
      end
    end,
    Span = function(span)
      if span.attributes.qmd ~= nil or span.attributes["qmd-base64"] ~= nil then
        local doc = process_quarto_markdown_input_element(span)
        return doc.blocks[1].content
      end
    end
  }
end
-- asciidoc.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function asciidocFigure(image)

  -- the figure that we'll make
  local figure = pandoc.List()

  -- the identififer
  if image.attr.identifier and image.attr.identifier ~= '' then
    figure:extend({"[[" .. image.attr.identifier .. "]]\n"});
  end
  
  -- caption
  local captionText = nil
  if image.caption and #image.caption > 0 then
    captionText = pandoc.write(pandoc.Pandoc({image.caption}), "asciidoctor")
    captionText = captionText:gsub("\n", " ")
  end
  if captionText ~= nil then
    figure:extend({"." .. captionText .. "\n"  })
  end

  -- alt text (ok to use HTML entities since alt is expressly for HTML output)
  local altText = image.attr.attributes["alt"] or image.attr.attributes[kFigAlt] or ""
  altText = altText:gsub("\"", "&quot;")
  altText = altText:gsub("<", "&lt;")
  altText = altText:gsub(">", "&gt;")
  altText = altText:gsub("&", "&amp;")

  -- the figure itself
  figure:extend({"image::" .. image.src .. "[\"" .. altText .. "\"]"})

  return pandoc.RawBlock("asciidoc", table.concat(figure, ""))
end

function asciidocDivFigure(el) 

  local figure = pandoc.List({})
  local id = el.attr.identifier
  
  -- append everything before the caption
  local contents = tslice(el.content, 1, #el.content - 1)
  
  -- return the figure and caption
  local caption = refCaptionFromDiv(el)
  if caption then
    local renderedCaption = pandoc.write(pandoc.Pandoc({caption}), "asciidoctor")
    figure:insert(pandoc.RawBlock('asciidoc', '.' .. renderedCaption))
  end
  
  if id and id ~= '' then
    figure:insert(pandoc.RawBlock('asciidoc', '[#' .. id .. ']\n'))
  end
  
  tappend(figure, contents)
  return figure
end
-- meta.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- inject metadata
function layoutMetaInject()
  return {
    Meta = function(meta)
      
      -- inject caption, subfig, tikz
      metaInjectLatex(meta, function(inject)
        inject(
          usePackage("caption") .. "\n" ..
          usePackage("subcaption")
        )
        if layoutState.usingTikz then
          inject(usePackage("tikz"))
        end
      end)

      -- This indicates whether the text highlighting theme has a 'light/dark' variant
      -- if it doesn't adapt, we actually will allow the text highlighting theme to control
      -- the appearance of the code block (e.g. so solarized will get a consistent yellow bg)
      local adaptiveTextHighlighting = param('adaptive-text-highlighting', false)

      -- If the user specifies 'code-block-border-left: false'
      -- then we should't give the code blocks this treatment
      local kCodeBlockBorderLeft = 'code-block-border-left'
      local kCodeBlockBackground = 'code-block-bg'

      -- Track whether to show a border or background
      -- Both options could be undefined, true / false or set to a color value
      local useCodeBlockBorder = (adaptiveTextHighlighting and meta[kCodeBlockBorderLeft] == nil and meta[kCodeBlockBackground] == nil) or (meta[kCodeBlockBorderLeft] ~= nil and meta[kCodeBlockBorderLeft] ~= false)
      local useCodeBlockBg = meta[kCodeBlockBackground] ~= nil and meta[kCodeBlockBackground] ~= false

      -- if we're going to display a border or background
      -- we need to inject color handling as well as the 
      -- box definition for code blocks
      if (useCodeBlockBorder or useCodeBlockBg) then
        metaInjectLatex(meta, function(inject)
          inject(
            usePackageWithOption("tcolorbox", "skins,breakable")
          )
        end)

        -- figure out the shadecolor
        local shadeColor = nil
        local bgColor = nil

        if useCodeBlockBorder and meta[kCodeBlockBorderLeft] and type(meta[kCodeBlockBorderLeft]) ~= "boolean" then
          shadeColor = latexXColor(meta[kCodeBlockBorderLeft])
        end
        if useCodeBlockBg and meta[kCodeBlockBackground] and type(meta[kCodeBlockBackground]) ~= "boolean"  then
          bgColor = latexXColor(meta[kCodeBlockBackground])
        end

        -- ensure shadecolor is defined
        metaInjectLatex(meta, function(inject)
          if (shadeColor ~= nil) then
            inject(
              "\\@ifundefined{shadecolor}{\\definecolor{shadecolor}" .. shadeColor .. "}"
            )  
          else
            inject(
              "\\@ifundefined{shadecolor}{\\definecolor{shadecolor}{rgb}{.97, .97, .97}}"
            )  
          end
        end)

        metaInjectLatex(meta, function(inject)
          if (bgColor ~= nil) then
            inject(
              "\\@ifundefined{codebgcolor}{\\definecolor{codebgcolor}" .. bgColor .. "}"
            )  
          end
        end)

        -- set color options for code blocks ('Shaded')
        -- core options
        local options = {
          boxrule = '0pt',
          ['frame hidden'] = "",
          ['sharp corners'] = "",
          ['breakable'] = "",
          enhanced = "",
          ['borderline west'] = '{3pt}{0pt}{shadecolor}'
        }
        if bgColor then 
          options.colback = "{codebgcolor}"
        else 
          options['interior hidden'] = ""
        end

        if shadeColor then
          options['borderline west'] = '{3pt}{0pt}{shadecolor}'
        end
        
        -- redefined the 'Shaded' environment that pandoc uses for fenced 
        -- code blocks
        metaInjectLatexBefore(meta, function(inject)
          inject("\\ifdefined\\Shaded\\renewenvironment{Shaded}{\\begin{tcolorbox}[" .. tColorOptions(options) .. "]}{\\end{tcolorbox}}\\fi")
        end)
      end



      -- enable column layout (packages and adjust geometry)
      if (layoutState.hasColumns or marginReferences() or marginCitations()) and _quarto.format.isLatexOutput() then
        -- inject sidenotes package
        metaInjectLatex(meta, function(inject)
          inject(
            usePackage("sidenotes")
          )
          inject(
            usePackage("marginnote")
          )
        end)
        
        if marginCitations() and meta.bibliography ~= nil then 
          local citeMethod = param('cite-method', 'citeproc')
          if citeMethod == 'natbib' then
            metaInjectLatex(meta, function(inject)
              inject(
                usePackage("bibentry")
              )  
              inject(
                usePackage("marginfix")
              )  

            end)
            metaInjectLatex(meta, function(inject)
              inject(
                '\\nobibliography*'
              )
            end)
  
          elseif citeMethod == 'biblatex' then
            metaInjectLatex(meta, function(inject)
              inject(
                usePackage("biblatex")
              )  
            end)
          end
        end

        -- add layout configuration based upon the document class
        -- we will customize any koma templates that have no custom geometries 
        -- specified. If a custom geometry is specified, we're expecting the
        -- user to address the geometry and layout
        local documentclassRaw = readOption(meta, 'documentclass');
        if documentclassRaw ~= nil then 
          local documentclass = pandoc.utils.stringify(documentclassRaw)
          if documentclass == 'scrartcl' or documentclass == 'scrarticle' or 
             documentclass == 'scrlttr2' or documentclass == 'scrletter' or
             documentclass == 'scrreprt' or documentclass == 'scrreport' then
            oneSidedColumnLayout(meta)
          elseif documentclass == 'scrbook' then
            -- better compute sidedness and deal with it
            -- choices are one, two, or semi
            local side = booksidedness(meta)
            if side == 'one' then
              oneSidedColumnLayout(meta)
            else
              twoSidedColumnLayout(meta, side == 'semi')
            end
          end  
        end
      end
      return meta
    end
  }
end

function booksidedness(meta)
  local side = 'two'
  local classoption = readOption(meta, 'classoption')
  if classoption then
    for i, v in ipairs(classoption) do
      local option = pandoc.utils.stringify(v)
      if option == 'twoside=semi' then
        side = 'semi'
      elseif option == 'twoside' or option == 'twoside=on' or option == 'twoside=true' or option == 'twoside=yes' then
        side = 'two'
      elseif option == 'twoside=false' or option == 'twoside=no' or option == 'twoside=off' then
        side = 'one'
      end
    end
  end
  return side
end

function marginReferences() 
  return param('reference-location', 'document') == 'margin'
end 

function marginCitations()
  return param('citation-location', 'document') == 'margin'
end

function twoSidedColumnLayout(meta, oneside)
  baseGeometry(meta, oneside)
end

function oneSidedColumnLayout(meta)
  local classoption = readOption(meta, 'classoption')
  if classoption == nil then
    classoption = pandoc.List({})
  end

  -- set one sided if not sidedness not already set
  local sideoptions = classoption:filter(function(opt) 
    local text = pandoc.utils.stringify(opt)
    return text:find('oneside') == 1 or text:find('twoside') == 1
  end)
  
  if #sideoptions == 0 then
    classoption:insert('oneside')
    meta.classoption = classoption
  end
  
  baseGeometry(meta)
end

function baseGeometry(meta, oneside)

  -- customize the geometry
  if not meta.geometry then
    meta.geometry = pandoc.List({})
  end  
  local userDefinedGeometry = #meta.geometry ~= 0

  -- if only 'showframe' is passed, we can still modify the geometry
  if #meta.geometry == 1 then
    if #meta.geometry[1] == 1 then
      local val = meta.geometry[1][1]
      if val.t == 'Str' and val.text == 'showframe' then
        userDefinedGeometry = false
      end
    end
  end 

  if not userDefinedGeometry then
    -- if one side geometry is explicitly requested, the
    -- set that (used for twoside=semi)
    if oneside then
      tappend(meta.geometry, {"twoside=false"})
    end
      
    tappend(meta.geometry, geometryForPaper(meta.papersize))
  end
end

-- We will automatically compute a geometry for a papersize that we know about
function geometryForPaper(paperSize)
  if paperSize ~= nil then
    local paperSizeStr = paperSize[1].text
    local width = kPaperWidthsIn[paperSizeStr]
    if width ~= nil then
      return geometryFromPaperWidth(width)
    else
      return pandoc.List({})
    end
  else 
    return pandoc.List({})
  end
end

function geometryFromPaperWidth(paperWidth) 
  local geometry = pandoc.List({})
  geometry:insert(metaInlineStr('left=' .. left(paperWidth) .. 'in'))
  geometry:insert(metaInlineStr('marginparwidth=' .. marginParWidth(paperWidth) .. 'in'))
  geometry:insert(metaInlineStr('textwidth=' .. textWidth(paperWidth) .. 'in'))
  geometry:insert(metaInlineStr('marginparsep=' .. marginParSep(paperWidth) .. 'in'))
  return geometry
end

function metaInlineStr(str) 
  return pandoc.Inlines({pandoc.Str(str)})
end


-- We will only provide custom geometries for paper widths that we are 
-- aware of and that would work well for wide margins. Some sizes get
-- so small that there just isn't a good way to represent the margin layout
-- so we just throw up our hands and take the default geometry
kPaperWidthsIn = {
  a0 = 33.11,
  a1 = 23.39,
  a2 = 16.54,
  a3 = 11.69,
  a4 = 8.3,
  a5 = 5.83,
  a6 = 4.13,
  a7 = 2.91,
  a8 = 2.05,
  b0 = 39.37,
  b1 = 27.83,
  b2 = 19.69,
  b3 = 13.90,
  b4 = 9.84,
  b5 = 6.93,
  b6 = 4.92,
  b7 = 3.46,
  b8 = 2.44,
  b9 = 1.73,
  b10 = 1.22,
  letter = 8.5,
  legal = 8.5,
  ledger =  11,
  tabloid = 17,
  executive = 7.25
}

local kLeft = 1
local kMarginParSep = .3

function left(width)
  if width >= kPaperWidthsIn.a4 then
    return kLeft
  else
    return kLeft * width / kPaperWidthsIn.a4
  end
end

function marginParSep(width)
  if width >= kPaperWidthsIn.a6 then
    return kMarginParSep
  else
    return kMarginParSep * width / kPaperWidthsIn.a4
  end
end

function marginParWidth(width) 
  return (width - 2*left(width) - marginParSep(width)) / 3
end

function textWidth(width)
  return ((width - 2*left(width) - marginParSep(width)) * 2) / 3
end


-- width.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- parse a layout specification
function parseLayoutWidths(figLayout, figureCount)
  
  -- parse json
  figLayout = pandoc.List(quarto.json.decode(figLayout))
  
  -- if there are no tables then make a table and stick the items in it
  if not figLayout:find_if(function(item) return type(item) == "table" end) then
     figLayout = pandoc.List({figLayout})
  end
      
  -- validate that layout is now all rows
  if figLayout:find_if(function(item) return type(item) ~= "table" end) then
    error("Invalid figure layout specification " .. 
          "(cannot mix rows and items at the top level")
  end
  
  -- convert numbers to strings as appropriate
  figureLayoutCount = 0
  figLayout = figLayout:map(function(row)
    --- get the cols
    local cols = pandoc.List(row)
    
    -- see if we have a total numeric value (no strings)
    local numericTotal = 0
    for i=1,#cols do 
      local width = cols[i]
      if type(width) == "number" then
        numericTotal = numericTotal + math.abs(width)
      else
        numericTotal = 0
        break
      end
    end
    
      
    return cols:map(function(width)
      figureLayoutCount = figureLayoutCount + 1
      if type(width) == "number" then
        if numericTotal ~= 0 then
          width = round((width / numericTotal) * 100, 2)
        elseif width <= 1 then
          width = round(width * 100, 2)
        end
        width = tostring(width) .. "%"
      end
      -- negative widths are "spacers" so we need to bump our total fig count
      if isSpacerWidth(width) then
        figureCount = figureCount + 1
      end
      -- return the width
      return width
    end)
  end)
  
  -- if there aren't enough rows then extend using the last row as a template
  local figureGap = figureCount - figureLayoutCount
  if figureGap > 0 then
    local lastRow = figLayout[#figLayout]
    local rowsToAdd = math.ceil(figureGap/#lastRow)
    for i=1,rowsToAdd do
      figLayout:insert(lastRow:clone())
    end
  end
   
  -- return the layout
  return figLayout
  
end

function isSpacerWidth(width)
  return pandoc.text.sub(width, 1, 1) == "-"
end


-- convert widths to percentages
function widthsToPercent(layout, cols)
  
  -- for each row
  for _,row in ipairs(layout) do
    
    -- determine numeric widths (and their total) for the row
    local widths = pandoc.List()
    for _,fig in ipairs(row) do
      widths[#widths+1] = 0
      local width = attribute(fig, "width", nil)
      if width then
        width = tonumber(string.match(width, "^(-?[%d%.]+)"))
        if width then
          widths[#widths] = width
        end
      end
    end
    
    -- create virtual fig widths as needed and note the total width
    local defaultWidth = widths:find_if(function(width) return width > 0 end)
    if defaultWidth == nil then
      defaultWidth = 42 -- this value is arbitrary
    end
    local totalWidth = 0
    for i=1,cols do
      if (i > #widths) or widths[i] == 0 then
        widths[i] = defaultWidth
      end
      totalWidth = totalWidth + widths[i]
    end
    -- allocate widths
    for i,fig in ipairs(row) do
      local width = round((widths[i]/totalWidth) * 100, 1)
      fig.attr.attributes["width"] = 
         tostring(width) .. "%"
      fig.attr.attributes["height"] = nil
    end
    
  end
end


-- elements with a percentage width and no height have a 'layout percent'
-- which means then should be laid out at a higher level in the tree than
-- the individual figure element
function horizontalLayoutPercent(el)
  return sizeToPercent(el.attr.attributes["width"])
end

function transferImageWidthToCell(img, divEl)
  divEl.attr.attributes["width"] = img.attributes["width"]
  if sizeToPercent(attribute(img, "width", nil)) then
    img.attributes["width"] = nil
  end
  img.attributes["height"] = nil
end


-- latex.lua
-- Copyright (C) 2020-2022 Posit Software, PBC
kSideCaptionEnv = 'sidecaption'

function latexPanel(divEl, layout, caption)
  
   -- create container
  local panel = pandoc.Div({})
 
  -- begin container
  local env, pos = latexPanelEnv(divEl, layout)
  panel.content:insert(latexBeginEnv(env, pos));

  local capType = "fig"
  local locDefault = "bottom"
  if hasTableRef(divEl) then
    capType = "tbl"
    locDefault = "top"
  end
  local capLoc = capLocation(capType, locDefault)
  if (caption and capLoc == "top") then
    insertLatexCaption(divEl, panel.content, caption.content)
  end
  
   -- read vertical alignment and strip attribute
  local vAlign = validatedVAlign(divEl.attr.attributes[kLayoutVAlign])
  divEl.attr.attributes[kLayoutVAlign] = nil

  for i, row in ipairs(layout) do
    
    for j, cell in ipairs(row) do
      
      -- there should never be \begin{table} inside a panel (as that would 
      -- create a nested float). this can happen if knitr injected it as a 
      -- result of a captioned latex figure. in that case remove it
      cell = latexRemoveTableDelims(cell)
      
      -- process cell (enclose content w/ alignment)
      local endOfTable = i == #layout
      local endOfRow = j == #row
      local prefix, content, suffix = latexCell(cell, vAlign, endOfRow, endOfTable)
      panel.content:insert(prefix)
      local align = cell.attr.attributes[kLayoutAlign]
      if align == "center" then
        panel.content:insert(pandoc.RawBlock("latex", latexBeginAlign(align)))
      end
      tappend(panel.content, content)
      if align == "center" then
        panel.content:insert(pandoc.RawBlock("latex", latexEndAlign(align)))
      end
      panel.content:insert(suffix)
    end
    
  end
  
  -- surround caption w/ appropriate latex (and end the panel)
  if caption and capLoc == "bottom" then
    insertLatexCaption(divEl, panel.content, caption.content)
  end
  
  -- end latex env
  panel.content:insert(latexEndEnv(env));
  
  -- conjoin paragarphs 
  panel.content = latexJoinParas(panel.content)
 
  -- return panel
  return panel
  
end

-- determine the environment (and pos) to use for a latex panel
function latexPanelEnv(divEl, layout)
  
  -- defaults
  local env = latexFigureEnv(divEl)
  local pos = nil
  
  -- explicit figure panel
  if hasFigureRef(divEl) then
    pos = attribute(divEl, kFigPos, pos)
  -- explicit table panel
  elseif hasTableRef(divEl) then
    env = latexTableEnv(divEl)
  -- if there are embedded tables then we need to use table
  else 
    local haveTables = layout:find_if(function(row)
      return row:find_if(hasTableRef)
    end)
    if haveTables then
      env = latexTableEnv(divEl)
    end
  end

  return env, pos
  
end

-- conjoin paragraphs (allows % to work correctly between minipages or subfloats)
function latexJoinParas(content)
  local blocks = pandoc.List()
  for i,block in ipairs(content) do
    if block.t == "Para" and #blocks > 0 and blocks[#blocks].t == "Para" then
      tappend(blocks[#blocks].content, block.content)
    else
      blocks:insert(block)
    end
  end
  return blocks
end

function latexImageFigure(image)

  return renderLatexFigure(image, function(figure)
    
    -- make a copy of the caption and clear it
    local caption = image.caption:clone()
    tclear(image.caption)
    
    -- get align
    local align = figAlignAttribute(image)
   
    -- insert the figure without the caption
    local figureContent = { pandoc.Para({
      pandoc.RawInline("latex", latexBeginAlign(align)),
      image,
      pandoc.RawInline("latex", latexEndAlign(align)),
      pandoc.RawInline("latex", "\n")
    }) }
    
    -- return the figure and caption
    return figureContent, caption
    
  end)
end

function latexDivFigure(divEl)
  
  return renderLatexFigure(divEl, function(figure)
    
     -- get align
    local align = figAlignAttribute(divEl)

    -- append everything before the caption
    local blocks = tslice(divEl.content, 1, #divEl.content - 1)
    local figureContent = pandoc.List()
    if align == "center" then
      figureContent:insert(pandoc.RawBlock("latex", latexBeginAlign(align)))
    end
    tappend(figureContent, blocks)
    if align == "center" then
      figureContent:insert(pandoc.RawBlock("latex", latexEndAlign(align)))
    end
    
    -- return the figure and caption
    local caption = refCaptionFromDiv(divEl)
    if not caption then
      caption = pandoc.Inlines()
    end
    return figureContent, caption.content
   
  end)
  
end

function renderLatexFigure(el, render)
  
  -- create container
  local figure = pandoc.Div({})

  -- begin the figure
  local figEnv = latexFigureEnv(el)
  local figPos = latexFigurePosition(el, figEnv)

  figure.content:insert(latexBeginEnv(figEnv, figPos))
  
  -- get the figure content and caption inlines
  local figureContent, captionInlines = render(figure)  

  local capLoc = capLocation("fig", "bottom")  

  -- surround caption w/ appropriate latex (and end the figure)
  if captionInlines and inlinesToString(captionInlines) ~= "" then
    if capLoc == "top" then
      insertLatexCaption(el, figure.content, captionInlines)
      tappend(figure.content, figureContent)
    else
      tappend(figure.content, figureContent)
      insertLatexCaption(el, figure.content, captionInlines)
    end
  else
    tappend(figure.content, figureContent)
  end
  
  -- end figure
  figure.content:insert(latexEndEnv(figEnv))
  
  -- return the figure
  return figure
  
end

function latexCaptionEnv(el) 
  if el.attr.classes:includes(kSideCaptionClass) then
    return kSideCaptionEnv
  else
    return 'caption'
  end
end

function insertLatexCaption(divEl, content, captionInlines) 
  local captionEnv = latexCaptionEnv(divEl)
  markupLatexCaption(divEl, captionInlines, captionEnv)
  if captionEnv == kSideCaptionEnv then
    if #content > 1 then
      content:insert(2, pandoc.Para(captionInlines))
    else
      content:insert(#content, pandoc.Para(captionInlines))
    end
  else 
    content:insert(pandoc.Para(captionInlines))
  end
end

function latexWrapSignalPostProcessor(el, token) 
  -- this is a table div not in a panel note any caption environment
  tprepend(el.content, {pandoc.RawBlock('latex', '%quartopost-' .. token)});
  tappend(el.content, {pandoc.RawBlock('latex', '%/quartopost-' .. token)});
end

function latexMarkupCaptionEnv(el) 
  local captionEnv = latexCaptionEnv(el)
  if captionEnv == 'sidecaption' then
    latexWrapSignalPostProcessor(el, 'sidecaption-206BE349');
  end
end

        
function markupLatexCaption(el, caption, captionEnv)

  -- by default, just use the caption env
  if captionEnv == nil then
    captionEnv = 'caption'
  end

  local captionEnv = latexCaptionEnv(el)
  
  -- caption prefix (includes \\caption macro + optional [subcap] + {)
  local captionPrefix = pandoc.List({
    pandoc.RawInline("latex", "\\" .. captionEnv)
  })
  local figScap = attribute(el, kFigScap, nil)
  if figScap then
    captionPrefix:insert(pandoc.RawInline("latex", "["))
    tappend(captionPrefix, markdownToInlines(figScap))
    captionPrefix:insert(pandoc.RawInline("latex", "]"))
  end
  captionPrefix:insert(pandoc.RawInline("latex", "{"))
  tprepend(caption, captionPrefix)
  
  -- end the caption
  caption:insert(pandoc.RawInline("latex", "}"))
end

local kBeginSideNote = '\\marginnote{\\begin{footnotesize}'
function latexBeginSidenote(block) 
  if block == nil or block then
    return pandoc.RawBlock('latex', kBeginSideNote)
  else
    return pandoc.RawInline('latex', kBeginSideNote)
  end
end

local kEndSideNote = '\\end{footnotesize}}'
function latexEndSidenote(el, block)
  local offset = ''
  if el.attr ~= nil then
    local offsetValue = el.attr.attributes['offset']
    if offsetValue ~= nil then
      offset = '[' .. offsetValue .. ']'
    end  
  end
  if block == nil or block then
    return pandoc.RawBlock('latex', kEndSideNote .. offset)
  else
    return pandoc.RawInline('latex', kEndSideNote .. offset)
  end
end

function latexWrapEnvironment(el, env, inline) 
  tprepend(el.content, {latexBeginEnv(env, nil, inline)})
  tappend(el.content, {latexEndEnv(env, inline)})
end

function latexBeginAlign(align)
  if align == "center" then
    return "{\\centering "
  elseif align == "right" then
    return "\\hfill{} "      
  else
    return ""
  end
end

function latexEndAlign(align)
  if align == "center" then
    return "\n\n}"
  elseif align == "left" then
    return " \\hfill{}"
  else
    return ""
  end
end

function latexBeginEnv(env, pos, inline)
  local beginEnv = "\\begin{" .. env .. "}"
  if pos then
    if not string.find(pos, "^[%[{]") then
      pos = "[" .. pos .. "]"
    end
    beginEnv = beginEnv .. pos
  end
  if inline then
    return pandoc.RawInline("latex", beginEnv)
  else
    return pandoc.RawBlock("latex", beginEnv)
  end
end

function latexEndEnv(env, inline)
  if inline then
    return pandoc.RawInline("latex", "\\end{" .. env .. "}")
  else
    return pandoc.RawBlock("latex", "\\end{" .. env .. "}")
  end
end

function latexCell(cell, vAlign, endOfRow, endOfTable)

  -- figure out what we are dealing with
  local label = cell.attr.identifier
  local image = figureImageFromLayoutCell(cell)
  if (label == "") and image then
    label = image.attr.identifier
  end
  local isFigure = isFigureRef(label)
  local isTable = isTableRef(label)
  local isSubRef = hasRefParent(cell) or (image and hasRefParent(image))
  local tbl = tableFromLayoutCell(cell)
  
  -- determine width 
  local width = cell.attr.attributes["width"]
  
  -- derive prefix, content, and suffix
  local prefix = pandoc.List()
  local subcap = pandoc.List()
  local content = pandoc.List()
  local suffix = pandoc.List()

  -- sub-captioned always uses \subfloat
  if isSubRef then
    
    -- lift the caption out it it's current location and onto the \subfloat
    local caption = pandoc.List()
    
    -- see if it's a captioned figure
    if image and #image.caption > 0 then
      caption = image.caption:clone()
      tclear(image.caption)
    elseif tbl then
      caption = pandoc.utils.blocks_to_inlines(tbl.caption.long)
      tclear(tbl.caption.long)
      if tbl.caption.short then
        tclear(tbl.caption.short)
      end
      cell.content = { latexTabular(tbl, vAlign) }
    else
      local divCaption = refCaptionFromDiv(cell)
      if divCaption then
        caption = refCaptionFromDiv(cell).content
        cell.content = tslice(cell.content, 1, #cell.content-1)
      else
        caption = pandoc.List()
      end
    end
    
    -- subcap
    latexAppend(subcap, "\\subcaption{\\label{" .. label .. "}")
    tappend(subcap, caption)
    latexAppend(subcap, "}\n")
  end
  
  -- convert to latex percent as necessary
  width = asLatexSize(width)

  -- start the minipage
  local miniPageVAlign = latexMinipageValign(vAlign)
  latexAppend(prefix, "\\begin{minipage}" .. miniPageVAlign .. "{" .. width .. "}\n")

  local capType = "fig"
  local locDefault = "bottom"
  if isTable then
    capType = "tbl"
    locDefault = "top"
  end
  local capLoc = capLocation(capType, locDefault)

  if (capLoc == "top") then
    tappend(prefix, subcap)
  end

  -- if we aren't in a sub-ref we may need to do some special work to
  -- ensure that captions are correctly emitted
  local cellOutput = false;
  if not isSubRef then
    if image and #image.caption > 0 then
      local caption = image.caption:clone()
      markupLatexCaption(cell, caption)
      tclear(image.caption)
      content:insert(pandoc.RawBlock("latex", "\\raisebox{-\\height}{"))
      content:insert(pandoc.Para(image))
      content:insert(pandoc.RawBlock("latex", "}"))
      content:insert(pandoc.Para(caption))
      cellOutput = true
    elseif isFigure then
      local caption = refCaptionFromDiv(cell).content
      markupLatexCaption(cell, caption)
      content:insert(pandoc.RawBlock("latex", "\\raisebox{-\\height}{"))
      tappend(content, tslice(cell.content, 1, #cell.content-1))
      content:insert(pandoc.RawBlock("latex", "}"))
      content:insert(pandoc.Para(caption)) 
      cellOutput = true
    end
  end
  
  -- if we didn't find a special case then just emit everything
  if not cellOutput then
    tappend(content, cell.content)

    -- vertically align the minipage
    if miniPageVAlign == "[t]" and image ~= nil then
      tprepend(content, { pandoc.RawBlock("latex", "\\raisebox{-\\height}{")})
      tappend(content, { pandoc.RawBlock("latex", "}") })
    end  
  end

  if (capLoc == "bottom") then
    tappend(suffix, subcap)
  end

  -- close the minipage
  latexAppend(suffix, "\\end{minipage}%")
  
  latexAppend(suffix, "\n")
  if not endOfRow then
    latexAppend(suffix, "%")
  elseif not endOfTable then
    latexAppend(suffix, "\\newline")
  end
  latexAppend(suffix, "\n")
  
  -- ensure that pandoc doesn't write any nested figures
  for i,block in ipairs(content) do
    latexHandsoffFigure(block)
    content[i] = _quarto.ast.walk(block, {
      Para = latexHandsoffFigure
    })
  end
  
  return pandoc.Para(prefix), content, pandoc.Para(suffix)
  
end

function latexTabular(tbl, vAlign)
  
  -- convert to simple table
  tbl = pandoc.utils.to_simple_table(tbl)
  
  -- list of inlines
  local tabular = pandoc.List()
  
  -- vertically align the minipage
  local tabularVAlign = latexMinipageValign(vAlign)
 
  -- caption
  if #tbl.caption > 0 then
    latexAppend(tabular, "\\caption{")
    tappend(tabular, tbl.caption)
    latexAppend(tabular, "}\n")
  end
  
  -- header
  local aligns = table.concat(tbl.aligns:map(latexTabularAlign), "")
  latexAppend(tabular, "\\begin{tabular}" .. tabularVAlign .. "{" .. aligns .. "}\n")
  latexAppend(tabular, "\\toprule\n")
  
  -- headers (optional)
  local headers = latexTabularRow(tbl.headers)
  if latexTabularRowHasContent(headers) then
    latexTabularRowAppend(tabular, headers)
    latexAppend(tabular, "\\midrule\n")
  end
  
  -- rows
  for _,row in ipairs(tbl.rows) do
    latexTabularRowAppend(tabular, latexTabularRow(row))
  end
  
  -- footer
  latexAppend(tabular, "\\bottomrule\n")
  latexAppend(tabular, "\\end{tabular}")
  
  -- return tabular
  return pandoc.Para(tabular)
  
end

function latexTabularRow(row)
  local cells = pandoc.List()
  for _,cell in ipairs(row) do
    cells:insert(pandoc.utils.blocks_to_inlines(cell))
  end
  return cells
end

function latexTabularRowHasContent(row)
  for _,cell in ipairs(row) do
    if #cell > 0 then
      return true
    end
  end
  return false
end

function latexTabularRowAppend(inlines, row)
  for i,cell in ipairs(row) do
    tappend(inlines, cell)
    if i < #row then
      latexAppend(inlines, " & ")
    end
  end
  latexAppend(inlines, "\\\\\n")
end

function latexTabularAlign(align)
  if align == pandoc.AlignLeft then
    return "l"
  elseif align == pandoc.AlignRight then
    return "r"
  elseif align == pandoc.AlignCenter then
    return "c"
  else
    return "l"
  end
end

function latexAppend(inlines, latex)
  inlines:insert(pandoc.RawInline("latex", latex))
end

function latexHandsoffFigure(el)
  if discoverFigure(el, false) ~= nil then
    el.content:insert(pandoc.RawInline("markdown", "<!-- -->"))
  end
end

function latexRemoveTableDelims(el)
  return _quarto.ast.walk(el, {
    RawBlock = function(el)
      if _quarto.format.isRawLatex(el) then
        el.text = el.text:gsub("\\begin{table}[^\n]*\n", "")
        el.text = el.text:gsub("\\end{table}[^\n]*\n?", "")
        return el
      end
    end
  })
end

local kMarginFigureEnv = "marginfigure"

-- Computes the figure position for a figure environment
-- (margin figures, for example, don't support position since 
-- they're already in the margin)
function latexFigurePosition(el, env) 
  if env == kMarginFigureEnv then
    return nil
  else
    return attribute(el, kFigPos, nil)
  end
end

function latexFigureEnv(el) 
 -- Check whether the user has specified a figure environment
  local figEnv = attribute(el, kFigEnv, nil)
  if figEnv ~= nil then
    -- the user specified figure environment
    return figEnv
  else
    -- if not user specified, look for other classes which might determine environment
    local classes = el.classes
    for i,class in ipairs(classes) do

      -- a margin figure or aside
      if isMarginEnv(class) then 
        noteHasColumns()
        return kMarginFigureEnv
      end

      -- any column that resolves to full width
      if isStarEnv(class) then
        noteHasColumns()
        return "figure*"
      end
    end  

    -- the default figure environment
    return "figure"
  end
end

function latexOtherEnv(el)
    -- if not user specified, look for other classes which might determine environment
    local classes = el.classes
    if classes ~= nil then
      for i,class in ipairs(classes) do

        -- any column that resolves to full width
        if isStarEnv(class) then
          noteHasColumns()
          return "figure*"
        end
      end  
    end
    return nil
end

function latexTableEnv(el)
 
  local classes = el.classes
  for i,class in ipairs(classes) do

    -- a margin figure or aside
    if isMarginEnv(class) then 
      noteHasColumns()
      return "margintable"
    end

    -- any column that resolves to full width
    if isStarEnv(class) then
      noteHasColumns()
      return "table*"
    end
  end  

  -- the default figure environment
  return "table"
end


function isStarEnv(clz) 
  return (clz:match('^column%-screen') or clz:match('^column%-page')) and not clz:match('%-left$')
end

function isMarginEnv(clz) 
  return clz == 'column-margin' or clz == 'aside'
end

function latexMinipageValign(vAlign) 
  if vAlign == "top" then
   return "[t]"
  elseif vAlign == "bottom" then 
    return "[b]"
  elseif vAlign == "center" then 
    return "[c]"
  else
   return ""
  end
end

-- html.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function htmlPanel(divEl, layout, caption)
  
  -- outer panel to contain css and figure panel
  local divId = divEl.attr.identifier
  if divId == nil then
    divId = ''
  end

  local panel = pandoc.Div({}, pandoc.Attr(divId, divEl.attr.classes))
  panel.attr.classes:insert("quarto-layout-panel")
  
  -- enclose in figure if it's a figureRef
  if hasFigureRef(divEl) then
    panel.content:insert(pandoc.RawBlock("html", "<figure>"))
  end

  -- compute vertical alignment and remove attribute
  local vAlign = validatedVAlign(divEl.attr.attributes[kLayoutVAlign])
  local vAlignClass = vAlignClass(vAlign);
  divEl.attr.attributes[kLayoutVAlign] = nil
  
  -- layout
  for i, row in ipairs(layout) do
    
    local rowDiv = pandoc.Div({}, pandoc.Attr("", {"quarto-layout-row"}))

    -- add the vertical align element to this row
    if vAlignClass then
      rowDiv.attr.classes:insert(vAlignClass);
    end
  
    for i, cellDiv in ipairs(row) do
      
      -- add cell class
      cellDiv.attr.classes:insert("quarto-layout-cell")
      
      -- if it has a ref parent then give it another class
      -- (used to provide subcaption styling)
      if layoutCellHasRefParent(cellDiv) then
        cellDiv.attr.classes:insert("quarto-layout-cell-subref")
      end
      
      -- create css style for width
      local cellDivStyle = ""
      local width = cellDiv.attr.attributes["width"]
      local align = cellDiv.attr.attributes[kLayoutAlign]
      cellDiv.attr.attributes[kLayoutAlign] = nil
      cellDivStyle = cellDivStyle .. "flex-basis: " .. width .. ";"
      cellDiv.attr.attributes["width"] = nil
      local justify = flexAlign(align)
      cellDivStyle = cellDivStyle .. "justify-content: " .. justify .. ";"
      cellDiv.attr.attributes["style"] = cellDivStyle
      
      -- if it's a table then our table-inline style will cause table headers
      -- (th) to be centered. set them to left is they are default
      local tbl = tableFromLayoutCell(cellDiv)
      if tbl then
        tbl.colspecs = tbl.colspecs:map(function(spec)
          if spec[1] == pandoc.AlignDefault then
            spec[1] = pandoc.AlignLeft
          end
          return spec
        end)
      end
      
      -- add div to row
      rowDiv.content:insert(cellDiv)
    end
    
    -- add row to the panel
    panel.content:insert(rowDiv)
  end
  
  -- determine alignment
  local align = layoutAlignAttribute(divEl)
  divEl.attr.attributes[kLayoutAlign] = nil
  
  -- insert caption and </figure>
  if caption then
    if hasFigureRef(divEl) then
      local captionPara = pandoc.Para({})
      -- apply alignment if we have it
      local figcaption = "<figcaption>"
      captionPara.content:insert(pandoc.RawInline("html", figcaption))
      tappend(captionPara.content, caption.content)
      captionPara.content:insert(pandoc.RawInline("html", "</figcaption>"))
      if capLocation('fig', 'bottom') == 'bottom' then
        panel.content:insert(captionPara)
      else
        tprepend(panel.content, { captionPara })
      end
    else
      local panelCaption = pandoc.Div(caption, pandoc.Attr("", { "panel-caption" }))
      if hasTableRef(divEl) then
        panelCaption.attr.classes:insert("table-caption")
        if capLocation('tbl', 'top') == 'bottom' then
          panel.content:insert(panelCaption)
        else
          tprepend(panel.content, { panelCaption })
        end
      else
        panel.content:insert(panelCaption)
      end
    end
  end
  
  if hasFigureRef(divEl) then
    panel.content:insert(pandoc.RawBlock("html", "</figure>"))
  end
  
  -- return panel
  return panel
end

function htmlDivFigure(el)
  
  return renderHtmlFigure(el, function(figure)
    
    -- get figure
    local figure = tslice(el.content, 1, #el.content-1)

    -- get caption
    local caption = refCaptionFromDiv(el)
    if caption then
      caption = caption.content
    else
      caption = nil
    end

    return figure, caption    
  end)
  
end


function htmlImageFigure(image)

  return renderHtmlFigure(image, function(figure)
    
    -- make a copy of the caption and clear it
    local caption = image.caption:clone()
    tclear(image.caption)
   
    -- pandoc sometimes ends up with a fig prefixed title
    -- (no idea way right now!)
    if image.title == "fig:" or image.title == "fig-" then
      image.title = ""
    end
   
    -- insert the figure without the caption
    local figure = { pandoc.Para({image}) }
    

    return figure, caption
    
  end)
  
end


function renderHtmlFigure(el, render)

  -- capture relevant figure attributes then strip them
  local align = figAlignAttribute(el)
  local keys = tkeys(el.attr.attributes)
  for _,k in pairs(keys) do
    if isFigAttribute(k) then
      el.attr.attributes[k] = nil
    end
  end
  local figureAttr = {}
  local style = el.attr.attributes["style"]
  if style then
    figureAttr["style"] = style
    el.attributes["style"] = nil
  end

  -- create figure div
  local figureDiv = pandoc.Div({}, pandoc.Attr(el.attr.identifier, {}, figureAttr))

  -- remove identifier (it is now on the div)
  el.attr.identifier = ""
          
  -- apply standalone figure css
  figureDiv.attr.classes:insert("quarto-figure")
  figureDiv.attr.classes:insert("quarto-figure-" .. align)

  -- also forward any column or caption classes
  local currentClasses = el.attr.classes
  for _,k in pairs(currentClasses) do
    if isCaptionClass(k) or isColumnClass(k) then
      figureDiv.attr.classes:insert(k)
    end
  end

  -- begin figure
  figureDiv.content:insert(pandoc.RawBlock("html", "<figure>"))
  
  -- render (and collect caption)
  local figure, captionInlines = render(figureDiv)
  
  -- render caption
  if captionInlines and #captionInlines > 0 then
    local figureCaption = pandoc.Plain({})
    figureCaption.content:insert(pandoc.RawInline(
      "html", "<figcaption>"
    ))
    tappend(figureCaption.content, captionInlines) 
    figureCaption.content:insert(pandoc.RawInline("html", "</figcaption>"))
    if capLocation('fig', 'bottom') == 'top' then
      figureDiv.content:insert(figureCaption)
      tappend(figureDiv.content, figure)
    else
      tappend(figureDiv.content, figure)
      figureDiv.content:insert(figureCaption)
    end
  else
    tappend(figureDiv.content, figure)
  end
  
  -- end figure and return
  figureDiv.content:insert(pandoc.RawBlock("html", "</figure>"))
  return figureDiv
  
end


function appendStyle(el, style)
  local baseStyle = attribute(el, "style", "")
  if baseStyle ~= "" and not string.find(baseStyle, ";$") then
    baseStyle = baseStyle .. ";"
  end
  el.attr.attributes["style"] = baseStyle .. style
end

function flexAlign(align)
  if align == "left" then
    return "flex-start"
  elseif align == "center" then
    return "center"
  elseif align == "right" then
    return "flex-end"
  end
end

function vAlignClass(vAlign) 
  if vAlign == "top" then 
    return "quarto-layout-valign-top"
  elseif vAlign == "bottom" then
    return "quarto-layout-valign-bottom"
  elseif vAlign == "center" then
    return "quarto-layout-valign-center"
  end
end

-- wp.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function tableWpPanel(divEl, layout, caption)
  return tablePanel(divEl, layout, caption, {
    pageWidth = wpPageWidth()
  })
end


function wpDivFigure(div)
  
  -- options
  options = {
    pageWidth = wpPageWidth(),
  }

  -- determine divCaption handler (always left-align)
  local divCaption = nil
  if _quarto.format.isDocxOutput() then
    divCaption = docxDivCaption
  elseif _quarto.format.isOdtOutput() then
    divCaption = odtDivCaption
  end
  if divCaption then
    options.divCaption = function(el, align) return divCaption(el, "left") end
  end

  -- get alignment
  local align = figAlignAttribute(div)
  
  -- create the row/cell for the figure
  local row = pandoc.List()
  local cell = div:clone()
  transferImageWidthToCell(div, cell)
  row:insert(tableCellContent(cell, align, options))
  
  -- make the table
  local figureTable = pandoc.SimpleTable(
    pandoc.List(), -- caption
    { layoutTableAlign(align) },  
    {   1   },         -- full width
    pandoc.List(), -- no headers
    { row }            -- figure
  )
  
  -- return it
  return pandoc.utils.from_simple_table(figureTable)
  
end

function wpPageWidth()
  local width = param("page-width", nil)
  if width then 
    if (type(width) == 'table') then
      width = tonumber(pandoc.utils.stringify(width))
    end

    if not width then
      error("You must use a number for page-width")
    else
      return width
    end
  else
    return 6.5
  end
end
-- docx.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function tableDocxPanel(divEl, layout, caption)
  return tablePanel(divEl, layout, caption, {
    pageWidth = wpPageWidth(),
    rowBreak = docxRowBreak,
    divCaption = docxDivCaption
  })
end


function docxRowBreak()
  return pandoc.RawBlock("openxml", [[
<w:p>
  <w:pPr>
    <w:framePr w:w="0" w:h="0" w:vAnchor="margin" w:hAnchor="margin" w:xAlign="right" w:yAlign="top"/>
  </w:pPr>
</w:p>
]])
end


-- create a native docx caption 
function docxDivCaption(captionEl, align)
  local caption = pandoc.Para({
    pandoc.RawInline("openxml", docxParaStyles(align))
  })
  tappend(caption.content, captionEl.content)
  return caption
end

function docxParaStyles(align)
  local styles = "<w:pPr>\n"
  local captionAlign = docxAlign(align)
  if captionAlign then
    styles = styles .. 
        "<w:jc w:val=\"" .. captionAlign .. "\"/>\n"
  end  
  styles = styles ..
    "<w:spacing w:before=\"200\" />\n" ..
    "<w:pStyle w:val=\"ImageCaption\" />\n" ..
    "</w:pPr>\n"
  return styles
end

function docxAlign(align)
  if align == "left" then
    return "start"
  elseif align == "center" then
    return "center"
  elseif align == "right" then
    return "end"
  else
    return nil
  end
end



-- jats.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function jatsDivFigure(divEl)

  -- ensure that only valid elements are permitted
  local filteredEl = _quarto.ast.walk(divEl, {
    Header = function(el)
      return pandoc.Strong(el.content)
    end
  })

  local figure = pandoc.List({})
  local id = filteredEl.attr.identifier
  
  -- append everything before the caption
  local contents = tslice(filteredEl.content, 1, #filteredEl.content - 1)
  
  -- return the figure and caption
  local caption = refCaptionFromDiv(filteredEl)
  if not caption then
    caption = pandoc.Inlines()
  end
  
  -- convert fig-pos to jats position
  local position = jatsPosition(filteredEl)
  local posAttr = ""
  if position then
    posAttr = ' position="' .. position .. '"'
  end
  
  figure:insert(pandoc.RawBlock('jats', '<fig id="' .. id .. '"' .. posAttr .. '>'))
  figure:insert(pandoc.RawBlock('jats', '<caption>'))
  figure:insert(caption);
  figure:insert(pandoc.RawBlock('jats', '</caption>'))
  tappend(figure, contents)
  figure:insert(pandoc.RawBlock('jats', '</fig>'))
  return figure
end

function jatsPosition(el) 
    local figPos = attribute(el, kFigPos, nil)
    if figPos and figPos == 'h' and figPos == 'H' then
      return "anchor"
    else
      return "float"
    end
end
-- odt.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function tableOdtPanel(divEl, layout, caption)
  return tablePanel(divEl, layout, caption, {
    pageWidth = wpPageWidth(),
    divCaption = odtDivCaption
  })
end

-- create a native odt caption (note that because "opendocument" paragraphs
-- include their styles as an attribute, we need to stringify the captionEl
-- so that it can be embedded in a raw opendocument block
function odtDivCaption(captionEl, align)
  local caption = pandoc.RawBlock("opendocument", 
    "<text:p text:style-name=\"FigureCaption\">" ..
    pandoc.utils.stringify(captionEl) .. 
    "</text:p>"
  )
  return caption
end



-- pptx.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function pptxPanel(divEl, layout)
  
  -- create panel
  local panel = pandoc.Div({}, pandoc.Attr(divEl.attr.identifier, {"columns"}))
  
  -- add a column for each figure (max 2 columns will be displayed)
  local kMaxCols = 2
  for i, row in ipairs(layout) do
    for _, cell in ipairs(row) do
      -- break on kMaxCols
      if #panel.content == kMaxCols then
        break
      end
      
      -- add the column class
      cell.attr.classes:insert("column")
      
      -- add to the panel
      panel.content:insert(cell)
    end
  end
  
  -- return panel
  return panel
end

-- table.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function tablePanel(divEl, layout, caption, options)
  
  -- empty options by default
  if not options then
    options = {}
  end
  -- outer panel to contain css and figure panel
  local divId = divEl.attr.identifier
  if divId == nil then
    divId = ''
  end

  -- create panel
  local panel = pandoc.Div({}, pandoc.Attr(divId))

  -- layout
  for i, row in ipairs(layout) do
    
    local aligns = row:map(function(cell) 
      
      -- get the align
      local align = cell.attr.attributes[kLayoutAlign]
      
      -- in docx tables inherit their parent cell alignment (likely a bug) so 
      -- this alignment will force all columns in embedded tables to follow it.
      -- if the alignment is center this won't make for very nice tables, so
      -- we force it to pandoc.AlignDefault
      if tableFromLayoutCell(cell) and _quarto.format.isDocxOutput() and align == "center" then
        return pandoc.AlignDefault
      else
        return layoutTableAlign(align) 
      end
    end)
    local widths = row:map(function(cell) 
      -- propagage percents if they are provided
      local layoutPercent = horizontalLayoutPercent(cell)
      if layoutPercent then
        return layoutPercent / 100
      else
        return 0
      end
    end)

    local cells = pandoc.List()
    for _, cell in ipairs(row) do
      local align = cell.attr.attributes[kLayoutAlign]
      cells:insert(tableCellContent(cell, align, options))
    end
    
    -- make the table
    local panelTable = pandoc.SimpleTable(
      pandoc.List(), -- caption
      aligns,
      widths,
      pandoc.List(), -- headers
      { cells }
    )
    
    -- add it to the panel
    panel.content:insert(pandoc.utils.from_simple_table(panelTable))
    
    -- add empty text frame (to prevent a para from being inserted btw the rows)
    if i ~= #layout and options.rowBreak then
      panel.content:insert(options.rowBreak())
    end
  end
  
  -- insert caption
  if caption then
    if options.divCaption then
      caption = options.divCaption(caption)
    end
     panel.content:insert(caption)
  end

  -- return panel
  return panel
end


function tableCellContent(cell, align, options)
  
  -- there will be special code if this an image or table
  local image = figureImageFromLayoutCell(cell)
  local tbl = tableFromLayoutCell(cell)
  local isSubRef = hasRefParent(cell) or (image and hasRefParent(image))
 
  if image then
    -- convert layout percent to physical units (if we have a pageWidth)
    -- this ensures that images don't overflow the column as they have
    -- been observed to do in docx
    if options.pageWidth then
      local layoutPercent = horizontalLayoutPercent(cell)
      if layoutPercent then
        local inches = (layoutPercent/100) * options.pageWidth
        image.attr.attributes["width"] = string.format("%2.2f", inches) .. "in"
      end
    end
    
    -- rtf and odt don't write captions in tables so make this explicit
    if #image.caption > 0 and (_quarto.format.isRtfOutput() or _quarto.format.isOdtOutput()) then
      local caption = image.caption:clone()
      tclear(image.caption)
      local captionPara = pandoc.Para(caption)
      if options.divCaption then
        captionPara = options.divCaption(captionPara, align)
      end
      cell.content:insert(captionPara)
    end
    
    -- we've already aligned the image in a table cell so prevent 
    -- extended handling as it would create a nested table cell
    preventExtendedFigure(image)
  end
  
  if hasFigureRef(cell) then
    -- style div caption if there is a custom caption function
    if options.divCaption then
      local divCaption = options.divCaption(refCaptionFromDiv(cell), align)
      cell.content[#cell.content] = divCaption 
    end
    
    -- we've already aligned the figure in a table cell so prevent 
    -- extended handling as it would create a nested table cell
    preventExtendedFigure(cell)
  end
  
  if tbl then
    
   
    if align == "center" then
      
      -- force widths to occupy 100%
      layoutEnsureFullTableWidth(tbl)
      
      -- for docx output we've forced the alignment of this cell to AlignDefault
      -- above (see the comment in tablePanel for rationale). Forcing the 
      -- table to 100$% width (done right above) makes it appear "centered" so
      -- do the same for the caption
      if _quarto.format.isDocxOutput() then
        local caption = pandoc.utils.blocks_to_inlines(tbl.caption.long)
        tclear(tbl.caption.long)
        if tbl.caption.short then
          tclear(tbl.caption.short)
        end
        cell.content:insert(1, options.divCaption(pandoc.Para(caption), align))
      end
    end
    
    -- workaround issue w/ docx nested tables: https://github.com/jgm/pandoc/issues/6983
    if _quarto.format.isDocxOutput() then
      if PANDOC_VERSION < pandoc.types.Version("2.11.3.2") then
        cell.content:insert(options.rowBreak())
      end
    end
  end
 
  return { cell }
  
end


function layoutTableAlign(align)
  if align == "left" then
    return pandoc.AlignLeft
  elseif align == "center" then
    return pandoc.AlignCenter
  elseif align == "right" then
    return pandoc.AlignRight
  end
end

function layoutEnsureFullTableWidth(tbl)
  if not tbl.colspecs:find_if(function(spec) return spec.width ~= nil end) then
    tbl.colspecs = tbl.colspecs:map(function(spec)
      return { spec[1], (1 / #tbl.colspecs) * 0.98 }
    end)
  end
end


-- figures.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- extended figure features including fig-align, fig-env, etc.
function extendedFigures() 
  return {
    
    Para = function(el)
      local image = discoverFigure(el, false)
      if image and shouldHandleExtendedImage(image) then
        if _quarto.format.isHtmlOutput() then
          return htmlImageFigure(image)
        elseif _quarto.format.isLatexOutput() then
          return latexImageFigure(image)
        elseif _quarto.format.isDocxOutput() then
          return wpDivFigure(createFigureDiv(el, image))
        elseif _quarto.format.isAsciiDocOutput() then
          return asciidocFigure(image)
        end
      end
    end,
    
    Div = function(el)
      if isFigureDiv(el) and shouldHandleExtended(el) then
        if _quarto.format.isLatexOutput() then
          return latexDivFigure(el)
        elseif _quarto.format.isHtmlOutput() then
          return htmlDivFigure(el)
        elseif _quarto.format.isDocxOutput() then
          return wpDivFigure(el)
        elseif _quarto.format.isJatsOutput() then
          return jatsDivFigure(el)
        elseif _quarto.format.isAsciiDocOutput() then
          return asciidocDivFigure(el)
        end
      end
    end
  }
end

local kFigExtended = "fig.extended"

function preventExtendedFigure(el)
  el.attr.attributes[kFigExtended] = "false"
end

function forceExtendedFigure(el) 
  el.attr.attributes[kFigExtended] = "true"
end

function shouldHandleExtended(el)
  return el.attr.attributes[kFigExtended] ~= "false"
end

-- By default, images without captions should be
-- excluded from extended processing. 
function shouldHandleExtendedImage(el) 
  -- handle extended if there is a caption
  if el.caption and #el.caption > 0 then
    return true
  end

  -- handle extended if there are fig- attributes
  local keys = tkeys(el.attr.attributes)
  for _,k in pairs(keys) do
    if isFigAttribute(k) then
      return true
    end
  end

  -- handle extended if there is column or caption 
  -- classes
  if hasColumnClasses(el) then
    return true
  end

  -- handle extended if it was explicitly enabled
  if el.attr.attributes[kFigExtended] == "true" then
    return true
  end

  -- otherwise, by default, do not handle
  return false
end
-- cites.lua
-- Copyright (C) 2021-2022 Posit Software, PBC
  

function citesPreprocess() 
  return {
    
    Note = function(note) 
      if _quarto.format.isLatexOutput() and marginCitations() then
        return _quarto.ast.walk(note, {
          Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd)
            appendAtEnd(citePlaceholderInline(citation))
          end)
        })
      end
    end,

    Para = function(para)
      local figure = discoverFigure(para, true)
      if figure and _quarto.format.isLatexOutput() and hasFigureRef(figure) then
        if hasMarginColumn(figure) or hasMarginCaption(figure) then
          -- This is a figure in the margin itself, we need to append citations at the end of the caption
          -- without any floating
          para.content[1] = _quarto.ast.walk(figure, {
              Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd)
                appendAtEnd(citePlaceholderInlineWithProtection(citation))
              end)
            })
          return para
        elseif marginCitations() then
          -- This is a figure is in the body, but the citation should be in the margin. Use 
          -- protection to shift any citations over
          para.content[1] = _quarto.ast.walk(figure, {
            Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd)
              appendInline(marginCitePlaceholderWithProtection(citation))
            end)
          })
          return para
        end   
      end
    end,

    Div = function(div)
      if _quarto.format.isLatexOutput() and hasMarginColumn(div) or marginCitations() then
        if hasTableRef(div) then 
          -- inspect the table caption for refs and just mark them as resolved
          local table = discoverTable(div)
          if table ~= nil and table.caption ~= nil and table.caption.long ~= nil then
            local cites = false
            -- go through any captions and resolve citations into latex
            for i, caption in ipairs(table.caption.long) do
              cites = resolveCaptionCitations(caption.content, hasMarginColumn(div)) or cites
            end
            if cites then
              return div
            end
          end  
        else
          return _quarto.ast.walk(div, {
            Inlines = walkUnresolvedCitations(function(citation, appendInline, appendAtEnd)
              if hasMarginColumn(div) then
                appendAtEnd(citePlaceholderInline(citation))
              end
            end)
          })
        end 

      end
    end
    
  }
end

function cites() 
  return {
    -- go through inlines and resolve any unresolved citations
    Inlines = walkUnresolvedCitations(function(citation, appendInline)
      appendInline(marginCitePlaceholderInline(citation))
    end)
  }
end

function walkUnresolvedCitations(func)
  return function(inlines)
    local modified = false
    if _quarto.format.isLatexOutput() and marginCitations() then
      for i,inline in ipairs(inlines) do
        if inline.t == 'Cite' then
          for j, citation in ipairs(inline.citations) do
            if not isResolved(citation) then
              func(
                citation, 
                function(inlineToAppend)
                  if inlineToAppend ~= nil then
                    local inlinePos = i
                    local citationCount = j                  
                    inlines:insert(inlinePos+citationCount, inlineToAppend)
                    modified = true
                    setResolved(citation)
                  end
                end,
                function(inlineToAppendAtEnd)
                  if inlineToAppendAtEnd ~= nil then
                    inlines:insert(#inlines + 1, inlineToAppendAtEnd)
                    modified = true
                    setResolved(citation)
                  end
                end
            )
            end  
          end
        end
      end
    end
    if modified then
      return inlines  
    end    
  end
end

function resolveCaptionCitations(captionContentInlines, inMargin)
  local citeEls = pandoc.List()
  for i,inline in ipairs(captionContentInlines) do
    if inline.t == 'Cite' then
      for j, citation in ipairs(inline.citations) do
        if inMargin then
          citeEls:insert(citePlaceholderInlineWithProtection(citation))
        else
          citeEls:insert(marginCitePlaceholderWithProtection(citation))
        end
        setResolved(citation)
      end
    end
  end

  if #citeEls > 0 then
    tappend(captionContentInlines, citeEls)
    return true
  else
    return false
  end
end

function marginCitePlaceholderInline(citation)
  return pandoc.RawInline('latex', '\\marginpar{\\begin{footnotesize}{?quarto-cite:'.. citation.id .. '}\\vspace{2mm}\\par\\end{footnotesize}}')
end

function citePlaceholderInline(citation)
  return pandoc.RawInline('latex', '\\linebreak\\linebreak{?quarto-cite:'.. citation.id .. '}\\linebreak')
end

function citePlaceholderInlineWithProtection(citation)
  return pandoc.RawInline('latex', '\\linebreak\\linebreak\\protect{?quarto-cite:'.. citation.id .. '}\\linebreak')
end

function marginCitePlaceholderWithProtection(citation)
  return pandoc.RawInline('latex', '\\protect\\marginnote{\\begin{footnotesize}\\protect{?quarto-cite:'.. citation.id .. '}\\linebreak\\end{footnotesize}}')
end

local resolvedCites = {}

function keyForCite(citation) 
  local id = citation.id
  local num = citation.note_num
  local key = id .. num
  return key
end

-- need a way to communicate that this has been resolved
function setResolved(citation)
  resolvedCites[keyForCite(citation)] = true
end

function isResolved(citation)
  return resolvedCites[keyForCite(citation)] == true
end

function discoverTable(div) 
  local tbl = div.content[1]
  if tbl.t == 'Table' then
    return tbl
  else
    return nil
  end
end
-- columns.lua
-- Copyright (C) 2021-2022 Posit Software, PBC


kSideCaptionClass = 'margin-caption'

function columns() 
  
  return {

    Div = function(el)  
      -- for any top level divs, render then
      renderDivColumn(el)
      return el      
    end,

    Span = function(el)
      -- a span that should be placed in the margin
      if _quarto.format.isLatexOutput() and hasMarginColumn(el) then 
        noteHasColumns()
        tprepend(el.content, {latexBeginSidenote(false)})
        tappend(el.content, {latexEndSidenote(el, false)})
        return el
      else 
        -- convert the aside class to a column-margin class
        if el.attr.classes and tcontains(el.attr.classes, 'aside') then
          noteHasColumns()
          el.attr.classes = el.attr.classes:filter(function(attr) 
            return attr ~= "aside"
          end)
          tappend(el.attr.classes, {'column-margin'})
          return el
        end
      end
    end,

    RawBlock = function(el) 
      -- Implements support for raw <aside> tags and replaces them with
      -- our raw latex representation
      if _quarto.format.isLatexOutput() then
        if el.format == 'html' then
          if el.text == '<aside>' then 
            noteHasColumns()
            el = latexBeginSidenote()
          elseif el.text == '</aside>' then
            el = latexEndSidenote(el)
          end
        end
      end
      return el
    end
  }
end

function renderDivColumn(el) 

  -- for html output that isn't reveal...
  if _quarto.format.isHtmlOutput() and not _quarto.format.isHtmlSlideOutput() then

    -- For HTML output, note that any div marked an aside should
    -- be marked a column-margin element (so that it is processed 
    -- by post processors). 
    -- For example: https://github.com/quarto-dev/quarto-cli/issues/2701
    if el.attr.classes and tcontains(el.attr.classes, 'aside') then
      noteHasColumns()
      el.attr.classes = el.attr.classes:filter(function(attr) 
        return attr ~= "aside"
      end)
      tappend(el.attr.classes, {'column-margin'})
      return el
    end

  elseif el.identifier and el.identifier:find("^lst%-") then
    -- for listings, fetch column classes from sourceCode element
    -- and move to the appropriate spot (e.g. caption, container div)
    local captionEl = el.content[1]
    local codeEl = el.content[2]
    
    if captionEl and codeEl then
      local columnClasses = resolveColumnClasses(codeEl)
      if #columnClasses > 0 then
        noteHasColumns()
        removeColumnClasses(codeEl)

        for i, clz in ipairs(columnClasses) do 
          if clz == kSideCaptionClass and _quarto.format.isHtmlOutput() then
            -- wrap the caption if this is a margin caption
            -- only do this for HTML output since Latex captions typically appear integrated into
            -- a tabular type layout in latex documents
            local captionContainer = pandoc.Div({captionEl}, pandoc.Attr("", {clz}))
            el.content[1] = codeEl
            el.content[2] = captionContainer    
          else
            -- move to container
            el.attr.classes:insert(clz)
          end
        end
      end
    end

  elseif _quarto.format.isLatexOutput() and not requiresPanelLayout(el) then

    -- see if there are any column classes
    local columnClasses = resolveColumnClasses(el)
    if #columnClasses > 0 then
      noteHasColumns() 
      
      if el.attr.classes:includes('cell-output-display') and #el.content > 0 then
        -- this could be a code-display-cell
        local figOrTable = false
        for j=1,#el.content do
          local contentEl = el.content[j]

          -- wrap figures
          local figure = discoverFigure(contentEl, false)
          if figure ~= nil then
            applyFigureColumns(columnClasses, figure)
            figOrTable = true
          elseif contentEl.t == 'Div' and hasTableRef(contentEl) then
            -- wrap table divs
            latexWrapEnvironment(contentEl, latexTableEnv(el), false)
            figOrTable = true
          elseif contentEl.attr ~= nil and hasFigureRef(contentEl) then
            -- wrap figure divs
            latexWrapEnvironment(contentEl, latexFigureEnv(el), false)
            figOrTable = true
          elseif contentEl.t == 'Table' then
            -- wrap the table in a div and wrap the table environment around it
            local tableDiv = pandoc.Div({contentEl})
            latexWrapEnvironment(tableDiv, latexTableEnv(el), false)
            el.content[j] = tableDiv
            figOrTable = true
          end 
        end

        if not figOrTable then
          processOtherContent(el.content)
        end
      else

        
        -- this is not a code cell so process it
        if el.attr ~= nil then
          if hasTableRef(el) then
            latexWrapEnvironment(el, latexTableEnv(el), false)
          elseif hasFigureRef(el) then
            latexWrapEnvironment(el, latexFigureEnv(el), false)
          else
            -- Look in the div to see if it contains a figure
            local figure = nil
            for j=1,#el.content do
              local contentEl = el.content[j]
              if figure == nil then
                figure = discoverFigure(contentEl, false)
              end
            end
            if figure ~= nil then
              applyFigureColumns(columnClasses, figure)
            else
              processOtherContent(el)
            end
          end
        end
      end   
    else 
       -- Markup any captions for the post processor
      latexMarkupCaptionEnv(el);
    end
  end
end

function processOtherContent(el)
  if hasMarginColumn(el) then
    -- (margin notes)
    noteHasColumns()
    tprepend(el.content, {latexBeginSidenote()});
    tappend(el.content, {latexEndSidenote(el)})
  else 
    -- column classes, but not a table or figure, so 
    -- handle appropriately
    local otherEnv = latexOtherEnv(el)
    if otherEnv ~= nil then
      latexWrapEnvironment(el, otherEnv, false)
    end
  end
  removeColumnClasses(el)
end

function applyFigureColumns(columnClasses, figure)
  -- just ensure the classes are - they will be resolved
  -- when the latex figure is rendered
  addColumnClasses(columnClasses, figure)

  -- ensure that extended figures will render this
  forceExtendedFigure(figure)  
end
  

function hasColumnClasses(el) 
  return tcontains(el.attr.classes, isColumnClass) or hasMarginColumn(el)
end

function hasMarginColumn(el)
  if el.attr ~= nil and el.attr.classes ~= nil then
    return tcontains(el.attr.classes, 'column-margin') or tcontains(el.attr.classes, 'aside')
  else
    return false
  end
end

function hasMarginCaption(el)
  if el.attr ~= nil and el.attr.classes ~= nil then
    return tcontains(el.attr.classes, 'margin-caption')
  else
    return false
  end
end

function noteHasColumns() 
  layoutState.hasColumns = true
end

function notColumnClass(clz) 
  return not isColumnClass(clz)
end

function resolveColumnClasses(el) 
  return el.attr.classes:filter(isColumnClass)
end

function columnToClass(column)
  if column ~= nil then
    return 'column-' .. column[1].text
  else
    return nil
  end
end

function removeColumnClasses(el)
  if el.attr and el.attr.classes then
    for i, clz in ipairs(el.attr.classes) do 
      if isColumnClass(clz) then
        el.attr.classes:remove(i)
      end
    end  
  end
end

function addColumnClasses(classes, toEl) 
  removeColumnClasses(toEl)
  for i, clz in ipairs(classes) do 
    if isColumnClass(clz) then
      toEl.attr.classes:insert(clz)
    end
  end  
end

function removeCaptionClasses(el)
  for i, clz in ipairs(el.attr.classes) do 
    if isCaptionClass(clz) then
      el.attr.classes:remove(i)
    end
  end
end

function resolveCaptionClasses(el)
  local filtered = el.attr.classes:filter(isCaptionClass)
  if #filtered > 0 then
    return {'margin-caption'}
  else
    return {}
  end
end

function isCaptionClass(clz)
  return clz == 'caption-margin' or clz == 'margin-caption'
end

function isColumnClass(clz) 
  if clz == nil then
    return false
  elseif clz == 'aside' then
    return true
  else
    return clz:match('^column%-')
  end
end
-- columns-preprocess.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function columnsPreprocess() 
  return {
    Div = function(el)  
      
      if el.attr.classes:includes('cell') then      
        -- for code chunks that aren't layout panels, forward the column classes to the output
        -- figures or tables (otherwise, the column class should be used to layout the whole panel)
        resolveColumnClassesForCodeCell(el)
      else
        resolveColumnClassesForEl(el)
      end      
      return el      
    end,

    Para = function(el)
      local figure = discoverFigure(el, false)
      if figure then
        resolveElementForScopedColumns(figure, 'fig')
      end
      return el
    end  
  }
end

-- resolves column classes for an element
function resolveColumnClassesForEl(el) 
  -- ignore sub figures and sub tables
  if not hasRefParent(el) then    
    if hasFigureRef(el) then
      resolveElementForScopedColumns(el, 'fig')
    elseif hasTableRef(el) then
      resolveElementForScopedColumns(el, 'tbl')
    end
  end
end

-- forward column classes from code chunks onto their display / outputs
function resolveColumnClassesForCodeCell(el) 

  -- read the classes that should be forwarded
  local figClasses = computeClassesForScopedColumns(el, 'fig')
  local tblClasses = computeClassesForScopedColumns(el, 'tbl')
  local figCaptionClasses = computeClassesForScopedCaption(el, 'fig')
  local tblCaptionClasses = computeClassesForScopedCaption(el, 'tbl')

  if #tblClasses > 0 or #figClasses > 0 or #figCaptionClasses > 0 or #tblCaptionClasses > 0 then 
    noteHasColumns()
    
    if hasLayoutAttributes(el) then
      -- This is a panel, don't resolve any internal classes, only resolve 
      -- actually column classes for this element itself
      resolveColumnClassesForEl(el)
    else
      -- Forward the column classes inside code blocks
      for i, childEl in ipairs(el.content) do 
        if childEl.attr ~= nil and childEl.attr.classes:includes('cell-output-display') then

          -- look through the children for any figures or tables
          local forwarded = false
          for j, figOrTableEl in ipairs(childEl.content) do
            local figure = discoverFigure(figOrTableEl, false)
            if figure ~= nil then
              -- forward to figures
              applyClasses(figClasses, figCaptionClasses, el, childEl, figure, 'fig')
              forwarded = true
            elseif figOrTableEl.attr ~= nil and hasFigureRef(figOrTableEl) then
              -- forward to figure divs
              applyClasses(figClasses, figCaptionClasses, el, childEl, figOrTableEl, 'fig')
              forwarded = true
            elseif (figOrTableEl.t == 'Div' and hasTableRef(figOrTableEl)) then
              -- for a table div, apply the classes to the figOrTableEl itself
              applyClasses(tblClasses, tblCaptionClasses, el, childEl, figOrTableEl, 'tbl')
              forwarded = true
            elseif figOrTableEl.t == 'Table' then
              -- the figOrTableEl is a table, just apply the classes to the div around it
              applyClasses(tblClasses, tblCaptionClasses, el, childEl, childEl, 'tbl')
              forwarded = true
            end
          end

          -- no known children were discovered, apply the column classes to the cell output display itself
          if not forwarded then 
            
            -- figure out whether there are tables inside this element
            -- if so, use tbl scope, otherwise treat as a fig
            local tableCount = countTables(el)
            local scope = 'fig'
            if tableCount > 0 then
              scope = 'tbl'
            end

            -- forward the classes from the proper scope onto the cell-output-display div
            local colClasses = computeClassesForScopedColumns(el, scope)
            local capClasses = computeClassesForScopedCaption(el, scope)
            applyClasses(colClasses, capClasses, el, childEl, childEl, scope)

          end
        end
      end
    end
  end         
end

function applyClasses(colClasses, captionClasses, containerEl, colEl, captionEl, scope)
  if #colClasses > 0 then
    applyColumnClasses(colEl, colClasses, scope)
    clearColumnClasses(containerEl, scope)
  end
  if #captionClasses > 0 then
    applyCaptionClasses(captionEl, captionClasses, scope)
    clearCaptionClasses(containerEl, scope)
  end
end

function resolveElementForScopedColumns(el, scope) 
  local classes = computeClassesForScopedColumns(el, scope)
  if #classes > 0 then
    applyColumnClasses(el, classes, scope)
  end

  local captionClasses = computeClassesForScopedCaption(el, scope)
  if #captionClasses > 0 then
    applyCaptionClasses(el, captionClasses, scope)
  end
end

function clearColumnClasses(el, scope)
  removeColumnClasses(el)
  if scope ~= nil then
    removeScopedColumnClasses(el, scope)
  end
end

function clearCaptionClasses(el, scope) 
  removeCaptionClasses(el)
  if scope ~= nil then
    removeScopedCaptionClasses(el, scope)
  end
end

function applyCaptionClasses(el, classes, scope)
  -- note that we applied a column class
  noteHasColumns()

  -- clear existing columns
  removeCaptionClasses(el)
  if scope ~= nil then
    removeScopedCaptionClasses(el, scope)
  end

  -- write the resolve scopes
  tappend(el.attr.classes, classes)
end

function applyColumnClasses(el, classes, scope) 
  -- note that we applied a column class
  noteHasColumns()

  -- clear existing columns
  removeColumnClasses(el)
  if scope ~= nil then
    removeScopedColumnClasses(el, scope)
  end

  -- write the resolve scopes
  tappend(el.attr.classes, classes)
end

function computeClassesForScopedCaption(el, scope)
  local globalCaptionClasses = captionOption('cap-location')
  local elCaptionClasses = resolveCaptionClasses(el)
  local orderedCaptionClasses = {elCaptionClasses, globalCaptionClasses}

  -- if a scope has been provided, include that
  if scope ~= nil then
    local elScopedCaptionClasses = resolveScopedCaptionClasses(el, scope)
    local scopedCaptionClasses = captionOption(scope .. '-cap-location')
    tprepend(orderedCaptionClasses, {elScopedCaptionClasses, scopedCaptionClasses})
  end

  for i, classes in ipairs(orderedCaptionClasses) do 
    if #classes > 0 then
      return classes
    end
  end
  return {}
end

-- Computes the classes for a given element, given its scope
function computeClassesForScopedColumns(el, scope) 
  local columnGlobalClasses = columnOption('column')
  local columnElClasses = resolveColumnClasses(el)
  local orderedClasses = {columnElClasses, columnGlobalClasses}

  -- if a scope has been provided, include that
  if scope ~= nil then
    local scopedGlobalClasses = columnOption(scope .. '-column')
    local scopedElClasses = resolveScopedColumnClasses(el, scope)
    tprepend(orderedClasses, {scopedElClasses, scopedGlobalClasses})
  end
  
  for i, classes in ipairs(orderedClasses) do 
    if #classes > 0 then
      return classes
    end
  end
  return {}
end

-- reads a column option key and returns the value
-- as a table of strings 
function columnOption(key) 
  local value = option(key,  nil)
  if value == nil or #value < 1 then
    return {}
  else
    return {'column-' .. inlinesToString(value[1])}
  end
end

function captionOption(key)
  local value = option(key,  nil)
  if value ~= nil then
  end
  if value ~= nil and value[1].text == 'margin' then
    return {'margin-caption'}
  else
    return {}
  end
end

function mergedScopedColumnClasses(el, scope)
  local scopedClasses = resolveScopedColumnClasses(el, scope)
  if #scopedClasses == 0 then
    scopedClasses = scopedColumnClassesOption(scope)
  end
  return scopedClasses
end

function resolveScopedColumnClasses(el, scope)
  local filtered = el.attr.classes:filter(function(clz)
    return clz:match('^' .. scope .. '%-column%-')
  end)

  return tmap(filtered, function(clz)
    return clz:sub(5)
  end)
end

function resolveScopedCaptionClasses(el, scope)
  local filtered = el.attr.classes:filter(function(clz)
    return clz:match('^' .. scope .. '%-cap%-location%-')
  end)

  local mapped = tmap(filtered, function(clz)
    return clz:sub(18)
  end)
  
  if tcontains(mapped, 'margin') then
    return {'margin-caption'}
  else 
    return {}
  end
end

function removeScopedColumnClasses(el, scope) 
  for i, clz in ipairs(el.attr.classes) do 
    if clz:match('^' .. scope .. '%-column%-') then
      el.attr.classes:remove(i)
    end
  end
end

function removeScopedCaptionClasses(el, scope)
  for i, clz in ipairs(el.attr.classes) do 
    if clz:match('^' .. scope .. '%-cap%-location%-') then
      el.attr.classes:remove(i)
    end
  end  
end

function scopedColumnClassesOption(scope) 
  local clz = option(scope .. '-column', nil);
  if clz == nil then
    clz = option('column',  nil)
  end
  local column = columnToClass(clz)
  if column then
    return {column}
  else
    return {}
  end
end
-- layout.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- required version
-- PANDOC_VERSION:must_be_at_least '2.13'

-- global layout state
layoutState = {
  hasColumns = false,
}

function layoutPanels()

  return {
    Div = function(el)
      if requiresPanelLayout(el) then
        
        -- partition
        local preamble, cells, caption = partitionCells(el)
        
        -- derive layout
        local layout = layoutCells(el, cells)
        
        -- call the panel layout functions
        local panel
        if _quarto.format.isLatexOutput() then
          panel = latexPanel(el, layout, caption)
        elseif _quarto.format.isHtmlOutput() then
          panel = htmlPanel(el, layout, caption)
        elseif _quarto.format.isDocxOutput() then
          panel = tableDocxPanel(el, layout, caption)
        elseif _quarto.format.isOdtOutput() then
          panel = tableOdtPanel(el, layout, caption)
        elseif _quarto.format.isWordProcessorOutput() then
          panel = tableWpPanel(el, layout, caption)
        elseif _quarto.format.isPowerPointOutput() then
          panel = pptxPanel(el, layout)
        else
          panel = tablePanel(el, layout, caption)
        end

        -- transfer attributes from el to panel
        local keys = tkeys(el.attr.attributes)
        for _,k in pairs(keys) do
          if not isLayoutAttribute(k) then
            panel.attr.attributes[k] = el.attr.attributes[k]
          end
        end
        
        if #preamble > 0 then
          local div = pandoc.Div({})
          if #preamble > 0 then
            tappend(div.content, preamble)
          end
          div.content:insert(panel)
          return div
          
        -- otherwise just return the panel
        else
          return panel
        end
        
      end
    end
  }  
end


function requiresPanelLayout(divEl)
  
  if hasLayoutAttributes(divEl) then
    return true
  -- latex and html require special layout markup for subcaptions
  elseif (_quarto.format.isLatexOutput() or _quarto.format.isHtmlOutput()) and 
          divEl.attr.classes:includes("tbl-parent") then
    return true
  else 
    return false
  end
  
end


function partitionCells(divEl)
  
  local preamble = pandoc.List()
  local cells = pandoc.List()
  local caption = nil
  
  -- extract caption if it's a table or figure div
  if hasFigureOrTableRef(divEl) then
    caption = refCaptionFromDiv(divEl)
    divEl.content = tslice(divEl.content, 1, #divEl.content-1)
  end
  
  local heading = nil
  for _,block in ipairs(divEl.content) do
    
    if isPreambleBlock(block) then
      if block.t == "CodeBlock" and #preamble > 0 and preamble[#preamble].t == "CodeBlock" then
        preamble[#preamble].text = preamble[#preamble].text .. "\n" .. block.text
      else
        preamble:insert(block)
      end
    elseif block.t == "Header" then
      if _quarto.format.isRevealJsOutput() then
        heading = pandoc.Para({ pandoc.Strong(block.content)})
      else
        heading = block
      end
    else 
      -- ensure we are dealing with a div
      local cellDiv = nil
      if block.t == "Div" then
        -- if this has a single figure div then unwrap it
        if #block.content == 1 and 
           block.content[#block.content].t == "Div" and
           hasFigureOrTableRef(block.content[#block.content]) then
          cellDiv = block.content[#block.content]
        else
          cellDiv = block
        end
      
      else
        cellDiv = pandoc.Div(block)
      end
      
      -- special behavior for cells with figures (including ones w/o captions)
      local fig = figureImageFromLayoutCell(cellDiv)
      if fig then
        -- transfer width to cell
        transferImageWidthToCell(fig, cellDiv)
      end
      
      -- if we have a heading then insert it
      if heading then 
        cellDiv.content:insert(1, heading)
        heading = nil
      end

      -- if this is .cell-output-display that isn't a figure or table 
      -- then unroll multiple blocks
      if cellDiv.attr.classes:find("cell-output-display") and 
         #cellDiv.content > 1 and 
         not hasFigureOrTableRef(cellDiv) then
        for _,outputBlock in ipairs(cellDiv.content) do
          if outputBlock.t == "Div" then
            cells:insert(outputBlock)
          else
            cells:insert(pandoc.Div(outputBlock))
          end
        end
      else
        -- add the div
        cells:insert(cellDiv)
      end
      
    end
    
  end

  return preamble, cells, caption
  
end


function layoutCells(divEl, cells)
  
  -- layout to return (list of rows)
  local rows = pandoc.List()
  
  -- note any figure layout attributes
  local layoutRows = tonumber(attribute(divEl, kLayoutNrow, nil))
  local layoutCols = tonumber(attribute(divEl, kLayoutNcol, nil))
  local layout = attribute(divEl, kLayout, nil)
  
  -- default to 1 column if nothing is specified
  if not layoutCols and not layoutRows and not layout then
    layoutCols = 1
  end
  
  -- if there is layoutRows but no layoutCols then compute layoutCols
  if not layoutCols and layoutRows ~= nil then
    layoutCols = math.ceil(#cells / layoutRows)
  end
  
  -- check for cols
  if layoutCols ~= nil then
    for i,cell in ipairs(cells) do
      if math.fmod(i-1, layoutCols) == 0 then
        rows:insert(pandoc.List())
      end
      rows[#rows]:insert(cell)
    end
    -- convert width units to percentages
    widthsToPercent(rows, layoutCols)
    
  -- check for layout
  elseif layout ~= nil then
    -- parse the layout
    layout = parseLayoutWidths(layout, #cells)
    
    -- manage/perform next insertion into the layout
    local cellIndex = 1
    local function layoutNextCell(width)
      -- check for a spacer width (negative percent)
      if isSpacerWidth(width) then
        local cell = pandoc.Div({
          pandoc.Para({pandoc.Str(" ")}),
          pandoc.Para({})
        }, pandoc.Attr(
          "", 
          { "quarto-figure-spacer" }, 
          { width = pandoc.text.sub(width, 2, #width) }
        ))
        rows[#rows]:insert(cell)
      -- normal figure layout
      else
        local cell = cells[cellIndex]
        if cell then
          cellIndex = cellIndex + 1
          cell.attr.attributes["width"] = width
          cell.attr.attributes["height"] = nil
          rows[#rows]:insert(cell)
        end
      end
    end
  
    -- process the layout
    for _,item in ipairs(layout) do
      if cellIndex > #cells then
        break
      end
      rows:insert(pandoc.List())
      for _,width in ipairs(item) do
        layoutNextCell(width)
      end
    end
    
  end
  
  -- determine alignment
  local align = layoutAlignAttribute(divEl)
  
  -- some width and alignment handling
  rows = rows:map(function(row)
    return row:map(function(cell)
      
      -- percentage based layouts need to be scaled down so they don't overflow the page 
      local percentWidth = sizeToPercent(attribute(cell, "width", nil))
      if percentWidth then
        percentWidth = round(percentWidth,1)
        cell.attr.attributes["width"] = tostring(percentWidth) .. "%"
      end
      
      -- provide default alignment if necessary
      cell.attr.attributes[kLayoutAlign] = layoutCellAlignment(cell, align)
     
      -- return cell
      return cell
    end)
   
  end)  

  -- return layout
  return rows
  
end

function isPreambleBlock(el)
  return (el.t == "CodeBlock" and el.attr.classes:includes("cell-code")) or
         (el.t == "Div" and el.attr.classes:includes("cell-output-stderr"))
end
-- index.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- initialize the index
function initCrossrefIndex()
     
  -- compute section offsets
  local sectionOffsets = pandoc.List({0,0,0,0,0,0,0})
  local numberOffset = pandoc.List(param("number-offset", {}))
  for i=1,#sectionOffsets do
    if i > #numberOffset then
      break
    end
    sectionOffsets[i] = numberOffset[i]
  end
  
  -- initialize index
  crossref.index = {
    nextOrder = {},
    nextSubrefOrder = 1,
    section = sectionOffsets:clone(),
    sectionOffsets = sectionOffsets:clone(),
    numberOffset = sectionOffsets:clone(),
    entries = {},
    headings = pandoc.List()
  }
  
end

-- advance a chapter
function indexNextChapter(index, appendix)
   -- reset nextOrder to 1 for all types if we are in chapters mode
  if crossrefOption("chapters", false) then
    -- reset all of the cross type counters
    for k,v in pairs(crossref.index.nextOrder) do
      crossref.index.nextOrder[k] = 1
    end
  end
  -- if this is an appendix the note the start index
  if appendix == true and crossref.startAppendix == nil then
    crossref.startAppendix = index
  end
end

-- next sequence in index for type
function indexNextOrder(type)
  if not crossref.index.nextOrder[type] then
    crossref.index.nextOrder[type] = 1
  end
  local nextOrder = crossref.index.nextOrder[type]
  crossref.index.nextOrder[type] = crossref.index.nextOrder[type] + 1
  crossref.index.nextSubrefOrder = 1
  return {
    section = crossref.index.section:clone(),
    order = nextOrder
  }
end

function indexAddHeading(identifier)
  if identifier ~= nil and identifier ~= '' then
    crossref.index.headings:insert(identifier)
  end
end

-- add an entry to the index
function indexAddEntry(label, parent, order, caption, appendix)
  if caption ~= nil then
    caption = pandoc.List(caption)
  end
  crossref.index.entries[label] = {
    parent = parent,
    order = order,
    caption = caption,
    appendix = appendix
  }
end

-- advance a subref
function nextSubrefOrder()
  local order = { section = nil, order = crossref.index.nextSubrefOrder }
  crossref.index.nextSubrefOrder = crossref.index.nextSubrefOrder + 1
  return order
end

-- does our index already contain this element?
function indexHasElement(el)
  return crossref.index.entries[el.attr.identifier] ~= nil
end


-- filter to write the index
function writeIndex()
  return {
    Pandoc = function(doc)
      local indexFile = param("crossref-index-file")
      if indexFile ~= nil then
        if isQmdInput() then
          writeKeysIndex(indexFile)
        else
          writeFullIndex(indexFile)
        end   
      end
    end
  }
end

function writeKeysIndex(indexFile)
  local index = {
    entries = pandoc.List(),
  }
  for k,v in pairs(crossref.index.entries) do
    -- create entry 
    local entry = {
      key = k,
    }
    -- add caption if we have one
    if v.caption ~= nil then
      entry.caption = inlinesToString(v.caption)
    else
      entry.caption = ""
    end
    -- add entry
    index.entries:insert(entry)
  end
 
  -- write the index
  local json = quarto.json.encode(index)
  local file = io.open(indexFile, "w")
  if file then
    file:write(json)
    file:close()
  else
    warn('Error attempting to write crossref index')
  end
end


function writeFullIndex(indexFile)
  -- create an index data structure to serialize for this file 
  local index = {
    entries = pandoc.List(),
    headings = crossref.index.headings:clone()
  }

  -- add options if we have them
  if next(crossref.options) then
    index.options = {}
    for k,v in pairs(crossref.options) do
      if type(v) == "table" then
        if tisarray(v) and pandoc.utils.type(v) ~= "Inlines" then
          index.options[k] = v:map(function(item) return pandoc.utils.stringify(item) end)
        else
          index.options[k] = pandoc.utils.stringify(v)
        end
      else
        index.options[k] = v
      end
    end
  end

  -- write a special entry if this is a multi-file chapter with an id
  local chapterId = crossrefOption("chapter-id")
  
  if chapterId then
    chapterId = pandoc.utils.stringify(chapterId)

     -- chapter heading
    index.headings:insert(chapterId)

    -- chapter entry
    if refType(chapterId) == "sec" and param("number-offset") ~= nil then
      local chapterEntry = {
        key = chapterId,
        parent = nil,
        order = {
          number = 1,
          section = crossref.index.numberOffset
        }
      }
      index.entries:insert(chapterEntry)
    end
  end

  for k,v in pairs(crossref.index.entries) do
    -- create entry 
    local entry = {
      key = k,
      parent = v.parent,
      order = {
        number = v.order.order,
      }
    }
    -- add caption if we have one
    if v.caption ~= nil then
      entry.caption = inlinesToString(v.caption)
    else
      entry.caption = ""
    end
    -- add section if we have one
    if v.order.section ~= nil then
      entry.order.section = v.order.section
    end
    -- add entry
    index.entries:insert(entry)
  end
 
  -- write the index
  local json = quarto.json.encode(index)
  local file = io.open(indexFile, "w")
  if file then
    file:write(json)
    file:close()
  else
    warn('Error attempting to write crossref index')
  end
end
-- preprocess.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- figures and tables support sub-references. mark them up before
-- we proceed with crawling for cross-refs
function crossrefPreprocess()
  
  return {

    Header = function(el)
      crossref.maxHeading = math.min(crossref.maxHeading, el.level)
    end,

    Pandoc = function(doc)
      
      -- initialize autolabels table
      crossref.autolabels = pandoc.List()
      
      local walkRefs
      walkRefs = function(parentId)
        return {
          Div = function(el)
            if hasFigureOrTableRef(el) then
              
              -- provide error caption if there is none
              if not refCaptionFromDiv(el) then
                local err = pandoc.Para(noCaption())
                el.content:insert(err)
              end
              
              if parentId ~= nil then
                if refType(el.attr.identifier) == refType(parentId) then
                  el.attr.attributes[kRefParent] = parentId
                end
              else
                -- mark as table parent if required
                if isTableRef(el.attr.identifier) then
                  el.attr.classes:insert("tbl-parent")
                end
                el = _quarto.ast.walk(el, walkRefs(el.attr.identifier))
              end
            end
            return el
          end,

          Table = function(el)
            return preprocessTable(el, parentId)
          end,
          
          RawBlock = function(el)
            return preprocessRawTableBlock(el, parentId)
          end,

          Para = function(el)
            
            -- provide error caption if there is none
            local fig = discoverFigure(el, false)
            if fig and hasFigureRef(fig) and #fig.caption == 0 then
              if isFigureRef(parentId) then
                fig.caption:insert(emptyCaption())
                fig.title = "fig:" .. fig.title
              else
                fig.caption:insert(noCaption())
              end
            end
            
            -- if we have a parent fig: then mark it's sub-refs
            if parentId and isFigureRef(parentId) then
              local image = discoverFigure(el)
              if image and isFigureImage(image) then
                image.attr.attributes[kRefParent] = parentId
              end
            end
            
            return el
          end
        }
      end

      -- walk all blocks in the document
      for i,el in pairs(doc.blocks) do
        -- always wrap referenced tables in a div
        if el.t == "Table" then
          doc.blocks[i] = preprocessTable(el, nil)
        elseif el.t == "RawBlock" then
          doc.blocks[i] = preprocessRawTableBlock(el, nil)
        elseif el.t ~= "Header" then
          local parentId = nil
          if hasFigureOrTableRef(el) and el.content ~= nil then
            parentId = el.attr.identifier

            -- mark as parent
            if isTableRef(el.attr.identifier) then
              el.attr.classes:insert("tbl-parent")
            end
            
            -- provide error caption if there is none
            if not refCaptionFromDiv(el) then
              local err = pandoc.Para(noCaption())
              el.content:insert(err)
            end
          end
          doc.blocks[i] = _quarto.ast.walk(el, walkRefs(parentId))
        end
      end

      return doc

    end
  }
end
-- sections.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function sections()
  
  return {
    Header = function(el)
      
      -- index the heading
      indexAddHeading(el.attr.identifier)

      -- skip unnumbered
      if (el.classes:find("unnumbered")) then
        return el
      end
      
      -- cap levels at 7
      local level = math.min(el.level, 7)
      
      -- get the current level
      local currentLevel = currentSectionLevel()
      
      -- if this level is less than the current level
      -- then set subsequent levels to their offset
      if level < currentLevel then
        for i=level+1,#crossref.index.section do
          crossref.index.section[i] = crossref.index.sectionOffsets[i]
        end
      end
      
      -- increment the level counter
      crossref.index.section[level] = crossref.index.section[level] + 1
      
      -- if this is a chapter then notify the index (will be used to 
      -- reset type-counters if we are in "chapters" mode)
      if level == 1 then
        indexNextChapter(crossref.index.section[level], currentFileMetadataState().appendix)
      end
      
      -- if this has a section identifier then index it
      if refType(el.attr.identifier) == "sec" then
        local order = indexNextOrder("sec")
        indexAddEntry(el.attr.identifier, nil, order, el.content, currentFileMetadataState().appendix)
      end

      -- if the number sections option is enabled then emulate pandoc numbering
      local section = sectionNumber(crossref.index.section, level)
      if not _quarto.format.isEpubOutput() and numberSectionsOptionEnabled() and level <= numberDepth() then
        el.attr.attributes["number"] = section
      end
      
      -- number the section if required
      if (numberSections() and level <= numberDepth()) then
        local appendix = (level == 1) and currentFileMetadataState().appendix
        if appendix then
          el.content:insert(1, pandoc.Space())
          tprepend(el.content, crossrefOption("appendix-delim", stringToInlines(" —")))
        elseif level == 1 and not _quarto.format.isHtmlOutput() then
          el.content:insert(1, pandoc.Str(". "))
        else
          el.content:insert(1, pandoc.Space())
        end

        if _quarto.format.isHtmlOutput() then
          el.content:insert(1, pandoc.Span(
            stringToInlines(section),
            pandoc.Attr("", { "header-section-number"})
          ))
        else
          tprepend(el.content, stringToInlines(section))
        end

        if appendix then
          el.content:insert(1, pandoc.Space())
          tprepend(el.content, crossrefOption("appendix-title", stringToInlines("Appendix")))
        end

      end
      
      -- return 
      return el
    end
  }
end

function currentSectionLevel()
  -- scan backwards for the first non-zero section level
  for i=#crossref.index.section,1,-1 do
    local section = crossref.index.section[i]
    if section ~= 0 then
      return i
    end
  end
  
  -- if we didn't find one then we are at zero (no sections yet)
  return 0
end

function numberSections()
  return not _quarto.format.isLatexOutput() and 
         not _quarto.format.isMarkdownOutput() 
         and numberSectionsOptionEnabled()
end

function numberSectionsOptionEnabled()
  return param("number-sections", false)
end

function numberDepth() 
  return param("number-depth", 6)
end

-- figures.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- process all figures
function crossrefFigures()
  return {
    Div = function(el)
      if isFigureDiv(el) and isReferenceableFig(el) then
        local caption = refCaptionFromDiv(el)
        if caption then
          processFigure(el, caption.content)
        end
      end
      return el
    end,

    Para = function(el)
      local image = discoverFigure(el)
      if image and isFigureImage(image) then
        processFigure(image, image.caption)
      end
      return el
    end
  }
end


-- process a figure, re-writing it's caption as necessary and
-- adding it to the global index of figures
function processFigure(el, captionContent)
  -- get label and base caption
  local label = el.attr.identifier
  local caption = captionContent:clone()

  -- determine order, parent, and displayed caption
  local order
  local parent = el.attr.attributes[kRefParent]
  if (parent) then
    order = nextSubrefOrder()
    prependSubrefNumber(captionContent, order)
  else
    order = indexNextOrder("fig")
    if _quarto.format.isLatexOutput() then
      tprepend(captionContent, {
        pandoc.RawInline('latex', '\\label{' .. label .. '}')
      })
    elseif _quarto.format.isAsciiDocOutput() then
      el.attr.identifier = label
    else
      tprepend(captionContent, figureTitlePrefix(order))
    end
  end

  -- update the index
  indexAddEntry(label, parent, order, caption)
end


function figureTitlePrefix(order)
  return titlePrefix("fig", param("crossref-fig-title", "Figure"), order)
end
-- tables.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- process all tables (note that cross referenced tables are *always*
-- wrapped in a div so they can carry parent information and so that
-- we can create a hyperef target for latex)
function crossrefTables()
  return {
    Div = function(el)
      if isTableDiv(el) and isReferenceableTbl(el) then
        
        -- are we a parent of subrefs? If so then process the caption
        -- at the bottom of the div
        if hasSubRefs(el, "tbl") then
          
          local caption = refCaptionFromDiv(el)
          if not caption then
            caption = pandoc.Para(noCaption())
            el.content:insert(caption)
          end
          local captionClone = caption:clone().content
          local label = el.attr.identifier
          local order = indexNextOrder("tbl")
          prependTitlePrefix(caption, label, order)
          indexAddEntry(label, nil, order, captionClone)
          
        else
          -- look for various ways of expressing tables in a div
          local processors = { processMarkdownTable, processRawTable }
          for _, process in ipairs(processors) do
            local tblDiv = process(el)
            if tblDiv then
              return tblDiv
            end
          end
        end
      end
      -- default to just reflecting the div back
      return el
    end
  }
end

function preprocessRawTableBlock(rawEl, parentId)
  
  local function divWrap(el, label, caption)
    local div = pandoc.Div(el, pandoc.Attr(label))
    if parentId then
      div.attr.attributes[kRefParent] = parentId
      if caption then
        div.content:insert(pandoc.Para(stringToInlines(caption)))
      end
    end
    return div
  end
  
  if _quarto.format.isRawHtml(rawEl) and _quarto.format.isHtmlOutput() then
    local captionPattern = htmlTableCaptionPattern()
    local _, caption, _ = string.match(rawEl.text, captionPattern) 
    if caption then
      -- extract id if there is one
      local caption, label = extractRefLabel("tbl", caption)
      if label then
        -- remove label from caption
        rawEl.text = rawEl.text:gsub(captionPattern, "%1" .. caption:gsub("%%", "%%%%") .. "%3", 1)
      elseif parentId then
        label = autoRefLabel(parentId)
      end
        
      if label then
        return divWrap(rawEl, label)
      end
    end
  elseif _quarto.format.isRawLatex(rawEl) and _quarto.format.isLatexOutput() then
    
    -- remove knitr label
    local knitrLabelPattern = "\\label{tab:[^}]+} ?"
    rawEl.text = rawEl.text:gsub(knitrLabelPattern, "", 1)
    
    -- try to find a caption with an id
    local captionPattern = "(\\caption{)(.*)" .. refLabelPattern("tbl") .. "([^}]*})"
    local _, caption, label, _ = rawEl.text:match(captionPattern)
    if label then
      -- remove label from caption
      rawEl.text = rawEl.text:gsub(captionPattern, "%1%2%4", 1)
    elseif parentId then
      label = autoRefLabel(parentId)
    end
      
    if label then
      return divWrap(rawEl, label)
    end
      
  end
  
  return rawEl
  
end

function preprocessTable(el, parentId)
  
 -- if there is a caption then check it for a table suffix
  if el.caption.long ~= nil then
    local last = el.caption.long[#el.caption.long]
    if last and #last.content > 0 then
       -- check for tbl label
      local label = nil
      local caption, attr = parseTableCaption(last.content)
      if startsWith(attr.identifier, "tbl-") then
        -- set the label and remove it from the caption
        label = attr.identifier
        attr.identifier = ""
        last.content = createTableCaption(caption, attr)
   
        -- provide error caption if there is none
        if #last.content == 0 then
          if parentId then
            tappend(last.content, { emptyCaption() })
          else
            tappend(last.content, { noCaption() })
          end
        end
        
      -- if there is a parent then auto-assign a label if there is none 
      elseif parentId then
        label = autoRefLabel(parentId)
      end
     
      if label then
        -- wrap in a div with the label (so that we have a target
        -- for the tbl ref, in LaTeX that will be a hypertarget)
        local div = pandoc.Div(el, pandoc.Attr(label))
        
        -- propagate parent id if the parent is a table
        if parentId and isTableRef(parentId) then
          div.attr.attributes[kRefParent] = parentId
        end
        
        -- return the div
        return div
      end
    end
  end
  return el
end


function processMarkdownTable(divEl)
  for i,el in pairs(divEl.content) do
    if el.t == "Table" then
      if el.caption.long ~= nil and #el.caption.long > 0 then
        local label = divEl.attr.identifier
        local caption = el.caption.long[#el.caption.long]
        processMarkdownTableEntry(divEl, el, label, caption)
        return divEl
      end
    end
  end
  return nil
end

function processMarkdownTableEntry(divEl, el, label, caption)
  
  -- clone the caption so we can add a clean copy to our index
  local captionClone = caption.content:clone()

  -- determine order / insert prefix
  local order
  local parent = divEl.attr.attributes[kRefParent]
  if (parent) then
    order = nextSubrefOrder()
    prependSubrefNumber(caption.content, order)
  else
    order = indexNextOrder("tbl")
    prependTitlePrefix(caption, label, order)
  end

  -- add the table to the index
  indexAddEntry(label, parent, order, captionClone)
  
end



function processRawTable(divEl)
  -- look for a raw html or latex table
  for i,el in pairs(divEl.content) do
    local rawParentEl, rawEl, rawIndex = rawElement(divEl, el, i)
    if rawEl then
      local label = divEl.attr.identifier
      -- html table
      if _quarto.format.isRawHtml(rawEl) then
        local captionPattern = htmlTableCaptionPattern()
        local _, caption, _ = string.match(rawEl.text, captionPattern)
        if caption then
          
          local order
          local prefix
          local parent = divEl.attr.attributes[kRefParent]
          if (parent) then
            order = nextSubrefOrder()
            local subref = pandoc.List()
            prependSubrefNumber(subref, order)
            prefix = inlinesToString(subref)
          else
            order = indexNextOrder("tbl")
            prefix = pandoc.utils.stringify(tableTitlePrefix(order))
          end
          
          indexAddEntry(label, parent, order, stringToInlines(caption))
        
          rawEl.text = rawEl.text:gsub(captionPattern, "%1" .. prefix .. " %2%3", 1)
          rawParentEl.content[rawIndex] = rawEl
          return divEl
        end
      -- latex table
      elseif _quarto.format.isRawLatex(rawEl) then
        
        -- look for raw latex with a caption
        captionPattern = "\\caption{([^}]+)}"
        caption = string.match(rawEl.text, captionPattern)
        if caption then
           processLatexTable(divEl, rawEl, captionPattern, label, caption)
           rawParentEl.content[rawIndex] = rawEl
           return divEl
        end
      end
      break
    end
  end

  return nil
end

-- handle either a raw block or raw inline in first paragraph
function rawElement(divEl, el, index)
  if el.t == "RawBlock" then
    return divEl, el, index
  elseif el.t == "Para" and #el.content > 0 and el.content[1].t == "RawInline" then
    return el, el.content[1], 1
  end
end

-- is this a Div containing a table?
function isTableDiv(el)
  return el.t == "Div" and hasTableRef(el)
end


function tableTitlePrefix(order)
  return titlePrefix("tbl", "Table", order)
end


function processLatexTable(divEl, el, captionPattern, label, caption)
  
  local order
  local parent = divEl.attr.attributes[kRefParent]
  if (parent) then
    el.text = el.text:gsub(captionPattern, "", 1)
    divEl.content:insert(pandoc.Para(stringToInlines(caption)))
    order = nextSubrefOrder()
  else
    el.text = el.text:gsub(captionPattern, "\\caption{\\label{" .. label .. "}" .. caption:gsub("%%", "%%%%") .. "}", 1)
    order = indexNextOrder("tbl")
  end
  
  indexAddEntry(label, parent, order, stringToInlines(caption))
end

function prependTitlePrefix(caption, label, order)
  if _quarto.format.isLatexOutput() then
     tprepend(caption.content, {
       pandoc.RawInline('latex', '\\label{' .. label .. '}')
     })
  elseif not _quarto.format.isAsciiDocOutput() then
     tprepend(caption.content, tableTitlePrefix(order))
  end
end


-- equations.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- process all equations
function equations()
  return {
    Para = processEquations,
    Plain = processEquations
  }
end

function processEquations(blockEl)

  -- alias inlines
  local inlines = blockEl.content

  -- do nothing if there is no math herein
  if inlines:find_if(isDisplayMath) == nil then
    return blockEl
  end

  local mathInlines = nil
  local targetInlines = pandoc.List()

  for i, el in ipairs(inlines) do

    -- see if we need special handling for pending math, if
    -- we do then track whether we should still process the
    -- inline at the end of the loop
    local processInline = true
    if mathInlines then
      if el.t == "Space" then
        mathInlines:insert(el)
        processInline = false
      elseif el.t == "Str" and refLabel("eq", el) then

        -- add to the index
        local label = refLabel("eq", el)
        local order = indexNextOrder("eq")
        indexAddEntry(label, nil, order)

        -- get the equation
        local eq = mathInlines[1]

        -- write equation
        if _quarto.format.isLatexOutput() then
          targetInlines:insert(pandoc.RawInline("latex", "\\begin{equation}"))
          targetInlines:insert(pandoc.Span(pandoc.RawInline("latex", eq.text), pandoc.Attr(label)))
          targetInlines:insert(pandoc.RawInline("latex", "\\label{" .. label .. "}\\end{equation}"))
        else
          local eqNumber = eqQquad
          local mathMethod = param("html-math-method", nil)
          if _quarto.format.isHtmlOutput() and (mathMethod == "mathjax" or mathMethod == "katex") then
            eqNumber = eqTag
          end
          eq.text = eq.text .. " " .. eqNumber(inlinesToString(numberOption("eq", order)))
          local span = pandoc.Span(eq, pandoc.Attr(label))
          targetInlines:insert(span)
        end

        -- reset state
        mathInlines = nil
        processInline = false
      else
        targetInlines:extend(mathInlines)
        mathInlines = nil
      end
    end

    -- process the inline unless it was already taken care of above
    if processInline then
      if isDisplayMath(el) then
          mathInlines = pandoc.List()
          mathInlines:insert(el)
        else
          targetInlines:insert(el)
      end
    end

  end

  -- flush any pending math inlines
  if mathInlines then
    targetInlines:extend(mathInlines)
  end

  -- return the processed list
  blockEl.content = targetInlines
  return blockEl
 
end

function eqTag(eq)
  return "\\tag{" .. eq .. "}"
end

function eqQquad(eq)
  return "\\qquad(" .. eq .. ")"
end

function isDisplayMath(el)
  return el.t == "Math" and el.mathtype == "DisplayMath"
end
-- listings.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- constants for list attributes
kLstCap = "lst-cap"
-- local kDataCodeAnnonationClz = 'code-annotation-code'

-- process all listings
function listings()
  
  return {
    DecoratedCodeBlock = function(node)
      local el = node.code_block
      local label = string.match(el.attr.identifier, "^lst%-[^ ]+$")
      local caption = el.attr.attributes[kLstCap]
      if label and caption then
        -- the listing number
        local order = indexNextOrder("lst")
        
        -- generate content from markdown caption
        local captionContent = markdownToInlines(caption)

        -- add the listing to the index
        indexAddEntry(label, nil, order, captionContent)

        node.caption = captionContent
        node.order = order
        return node
      end
      return nil
    end,

    CodeBlock = function(el)
      local label = string.match(el.attr.identifier, "^lst%-[^ ]+$")
      local caption = el.attr.attributes[kLstCap]
      if label and caption then
    
        -- the listing number
        local order = indexNextOrder("lst")
        
        -- generate content from markdown caption
        local captionContent = markdownToInlines(caption)
        
        -- add the listing to the index
        indexAddEntry(label, nil, order, captionContent)
       
        if _quarto.format.isLatexOutput() then

          -- add listing class to the code block
          el.attr.classes:insert("listing")

          -- if we are use the listings package we don't need to do anything
          -- further, otherwise generate the listing div and return it
          if not latexListings() then
            local listingDiv = pandoc.Div({})
            local env = "\\begin{codelisting}"
            if el.classes:includes('code-annotation-code') then
              env = env .. "[H]"
            end
            listingDiv.content:insert(pandoc.RawBlock("latex", env))
            local listingCaption = pandoc.Plain({pandoc.RawInline("latex", "\\caption{")})
            listingCaption.content:extend(captionContent)
            listingCaption.content:insert(pandoc.RawInline("latex", "}"))
            listingDiv.content:insert(listingCaption)
            listingDiv.content:insert(el)
            listingDiv.content:insert(pandoc.RawBlock("latex", "\\end{codelisting}"))
            return listingDiv
          end

        else
         
           -- Prepend the title
          tprepend(captionContent, listingTitlePrefix(order))

          -- return a div with the listing
          return pandoc.Div(
            {
              pandoc.Para(captionContent),
              el
            },
            pandoc.Attr(label, {"listing"})
          )
        end

      end
      
      --  if we get this far then just reflect back the el
      return el
    end
  }

end

function listingTitlePrefix(order)
  return titlePrefix("lst", "Listing", order)
end


function latexListings()
  return param("listings", false)
end
-- theorems.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- preprocess theorem to ensure that embedded headings are unnumered
function crossrefPreprocessTheorems()
  local types = theoremTypes
  return {
    Div = function(el)
      local type = refType(el.attr.identifier)
      if types[type] ~= nil or proofType(el) ~= nil then
        return _quarto.ast.walk(el, {
          Header = function(el)
            el.classes:insert("unnumbered")
            return el
          end
        })
      end

    end
  }
end

function crossrefTheorems()

  local types = theoremTypes

  return {
    Div = function(el)

      local type = refType(el.attr.identifier)
      local theoremType = types[type]
      if theoremType then

        -- add class for type
        el.attr.classes:insert("theorem")
        if theoremType.env ~= "theorem" then
          el.attr.classes:insert(theoremType.env)
        end
        
        -- capture then remove name
        local name = markdownToInlines(el.attr.attributes["name"])
        if not name or #name == 0 then
          name = resolveHeadingCaption(el)
        end
        el.attr.attributes["name"] = nil 
        
        -- add to index
        local label = el.attr.identifier
        local order = indexNextOrder(type)
        indexAddEntry(label, nil, order, name)
        
        -- If this theorem has no content, then create a placeholder
        if #el.content == 0 or el.content[1].t ~= "Para" then
          tprepend(el.content, {pandoc.Para({pandoc.Str '\u{a0}'})})
        end
      
        if _quarto.format.isLatexOutput() then
          local preamble = pandoc.Para(pandoc.RawInline("latex", 
            "\\begin{" .. theoremType.env .. "}"))
          preamble.content:insert(pandoc.RawInline("latex", "["))
          if name then
            tappend(preamble.content, name) 
          end
          preamble.content:insert(pandoc.RawInline("latex", "]"))
          preamble.content:insert(pandoc.RawInline("latex",
            "\\protect\\hypertarget{" .. label .. "}{}\\label{" .. label .. "}")
          )
          el.content:insert(1, preamble)
          el.content:insert(pandoc.Para(pandoc.RawInline("latex", 
            "\\end{" .. theoremType.env .. "}"
          )))
          -- Remove id on those div to avoid Pandoc inserting \hypertaget #3776
          el.attr.identifier = ""
        elseif _quarto.format.isJatsOutput() then

          -- JATS XML theorem
          local lbl = captionPrefix(nil, type, theoremType, order)
          el = jatsTheorem(el, lbl, name)          
          
        else
          -- create caption prefix
          local captionPrefix = captionPrefix(name, type, theoremType, order)
          local prefix =  { 
            pandoc.Span(
              pandoc.Strong(captionPrefix), 
              pandoc.Attr("", { "theorem-title" })
            )
          }

          -- prepend the prefix
          local caption = el.content[1]

          if caption.content == nil then
            -- https://github.com/quarto-dev/quarto-cli/issues/2228
            -- caption doesn't always have a content field; in that case,
            -- use the parent?
            tprepend(el.content, prefix)
          else
            tprepend(caption.content, prefix)
          end
        end

      else
        -- see if this is a proof, remark, or solution
        local proof = proofType(el)
        if proof ~= nil then

          -- ensure requisite latex is injected
          crossref.usingTheorems = true

          if proof.env ~= "proof" then
            el.attr.classes:insert("proof")
          end

          -- capture then remove name
          local name = markdownToInlines(el.attr.attributes["name"])
          if not name or #name == 0 then
            name = resolveHeadingCaption(el)
          end
          el.attr.attributes["name"] = nil 

          -- output
          if _quarto.format.isLatexOutput() then
            local preamble = pandoc.Para(pandoc.RawInline("latex", 
              "\\begin{" .. proof.env .. "}"))
            if name ~= nil then
              preamble.content:insert(pandoc.RawInline("latex", "["))
              tappend(preamble.content, name)
              preamble.content:insert(pandoc.RawInline("latex", "]"))
            end 
            el.content:insert(1, preamble)
            el.content:insert(pandoc.Para(pandoc.RawInline("latex", 
              "\\end{" .. proof.env .. "}"
            )))
          elseif _quarto.format.isJatsOutput() then
            el = jatsTheorem(el,  nil, name )
          else
            local span = pandoc.Span(
              { pandoc.Emph(pandoc.Str(envTitle(proof.env, proof.title)))},
              pandoc.Attr("", { "proof-title" })
            )
            if name ~= nil then
              span.content:insert(pandoc.Str(" ("))
              tappend(span.content, name)
              span.content:insert(pandoc.Str(")"))
            end
            tappend(span.content, { pandoc.Str(". ")})

            -- if the first block is a paragraph, then prepend the title span
            if #el.content > 0 and 
               el.content[1].t == "Para" and
               el.content[1].content ~= nil and 
               #el.content[1].content > 0 then
              el.content[1].content:insert(1, span)
            else
              -- else insert a new paragraph
              el.content:insert(1, pandoc.Para{span})
            end
          end

        end

      end
     
      return el
    
    end
  }

end

function jatsTheorem(el, label, title) 

  -- <statement>
  --   <label>Equation 2</label>
  --   <title>The Pythagorean</title>
  --   <p>
  --     ...
  --   </p>
  -- </statement> 

  if title then
    tprepend(el.content, {
      pandoc.RawBlock("jats", "<title>"),  
      pandoc.Plain(title), 
      pandoc.RawBlock("jats", "</title>")})
  end

  if label then
    tprepend(el.content, {
      pandoc.RawBlock("jats", "<label>"),  
      pandoc.Plain(label), 
      pandoc.RawBlock("jats", "</label>")})
  end
  
  -- Process the caption (if any)
  
  -- Emit the statement
  local stmtPrefix = pandoc.RawBlock("jats",  '<statement id="' .. el.attr.identifier .. '">')
  local stmtSuffix = pandoc.RawBlock("jats",  '</statement>')

  el.content:insert(1, stmtPrefix)
  el.content:insert(stmtSuffix)
  return el
end

function captionPrefix(name, type, theoremType, order) 
  local prefix = title(type, theoremType.title)
  table.insert(prefix, pandoc.Space())
  tappend(prefix, numberOption(type, order))
  table.insert(prefix, pandoc.Space())
  if name then
    table.insert(prefix, pandoc.Str("("))
    tappend(prefix, name)
    table.insert(prefix, pandoc.Str(")"))
    table.insert(prefix, pandoc.Space())
  end
  return prefix
end


-- theorem latex includes
function theoremLatexIncludes()
  
  -- determine which theorem types we are using
  local types = theoremTypes
  local refs = tkeys(crossref.index.entries)
  local usingTheorems = crossref.usingTheorems
  for k,v in pairs(crossref.index.entries) do
    local type = refType(k)
    if types[type] then
      usingTheorems = true
      types[type].active = true
    end
  end
  
  -- return requisite latex if we are using theorems
  if usingTheorems then
    local secType 
    if crossrefOption("chapters", false) then 
      secType = "chapter" 
    else 
      secType = "section" 
    end
    local theoremIncludes = "\\usepackage{amsthm}\n"
    for _, type in ipairs(tkeys(types)) do
      if types[type].active then
        theoremIncludes = theoremIncludes .. 
          "\\theoremstyle{" .. types[type].style .. "}\n" ..
          "\\newtheorem{" .. types[type].env .. "}{" .. 
          titleString(type, types[type].title) .. "}[" .. secType .. "]\n"
      end
    end
    theoremIncludes = theoremIncludes ..
      "\\theoremstyle{remark}\n" ..
      "\\AtBeginDocument{\\renewcommand*{\\proofname}{" .. envTitle("proof", "Proof") .. "}}\n" ..
      "\\newtheorem*{remark}{" .. envTitle("remark", "Remark") .. "}\n" ..
      "\\newtheorem*{solution}{" .. envTitle("solution", "Solution") .. "}\n"
    return theoremIncludes
  else
    return nil
  end
end

-- qmd.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function isQmdInput()
  return param("crossref-input-type", "md") == "qmd"
end

function qmd()
  if isQmdInput() then
    return {
      -- for qmd, look for label: and fig-cap: inside code block text
      CodeBlock = function(el)
        local label = el.text:match("|%slabel:%s(%a+%-[^\n]+)\n")
        if label ~= nil and (isFigureRef(label) or isTableRef(label)) then
          local type, caption = parseCaption(label, el.text)
          if type == "fig" or type == "tbl" then
            local order = indexNextOrder(type)
            indexAddEntry(label, nil, order, stringToInlines(caption))
          end
        end
        return el
      end
    }
  else
    return {}
  end
end

function parseCaption(label, elText)
  local type, caption = elText:match("|%s(%a+)%-cap:%s(.-)\n")
  if caption ~= nil then
    -- remove enclosing quotes (if any)
    if caption:sub(1, 1) == '"' then
      caption = caption:sub(2, #caption)
    end
    if caption:sub(#caption, #caption) == '"' then
      caption = caption:sub(1, #caption - 1)
    end
    -- replace escaped quotes
    caption = caption:gsub('\\"', '"')

    -- return
    return type, caption
  else
    return nil
  end
  
end
-- refs.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


-- resolve references
function resolveRefs()
  
  return {
    Cite = function(citeEl)
    
      -- all valid ref types (so we can provide feedback when one doesn't match)
      local refTypes = validRefTypes()
      
      -- scan citations for refs
      local refs = pandoc.List()
      for i, cite in ipairs (citeEl.citations) do
        -- get the label and type, and note if the label is uppercase
        local label = cite.id
        local type = refType(label)
        if type ~= nil and isValidRefType(type) then
          local upper = not not string.match(cite.id, "^[A-Z]")

          -- convert the first character of the label to lowercase for lookups
          label = pandoc.text.lower(label:sub(1, 1)) .. label:sub(2)
        
          -- lookup the label
          local resolve = param("crossref-resolve-refs", true)
          local entry = crossref.index.entries[label]
          if entry ~= nil or not resolve then
        
            -- preface with delimiter unless this is citation 1
            if (i > 1) then
              refs:extend(refDelim())
              refs:extend(stringToInlines(" "))
            end
  
            -- create ref text
            local ref = pandoc.List()
            if #cite.prefix > 0 then
              ref:extend(cite.prefix)
              ref:extend({nbspString()})
            elseif cite.mode ~= pandoc.SuppressAuthor then
              
              -- some special handling to detect chapters and use
              -- an alternate prefix lookup
              local prefixType = type
              local chapters = crossrefOption("chapters", false)
              if chapters and entry then
                if resolve and type == "sec" and isChapterRef(entry.order.section) then
                  if entry.appendix then
                    prefixType = "apx"
                  else
                    prefixType = "ch"
                  end
                end
              end
              if resolve or type ~= "sec" then
                local prefix = refPrefix(prefixType, upper)
                if #prefix > 0 then
                  ref:extend(prefix)
                  ref:extend({nbspString()})
                end
              end
              
            end
  
            -- for latex inject a \ref, otherwise format manually
            if _quarto.format.isLatexOutput() then
              ref:extend({pandoc.RawInline('latex', '\\ref{' .. label .. '}')})
            elseif _quarto.format.isAsciiDocOutput() then
              ref = pandoc.List({pandoc.RawInline('asciidoc', '<<' .. label .. '>>')})
            else
              if not resolve then
                local refClasses = pandoc.List({"quarto-unresolved-ref"})
                if #cite.prefix > 0 or cite.mode == pandoc.SuppressAuthor then
                  refClasses:insert("ref-noprefix")
                end
                local refSpan = pandoc.Span(
                  stringToInlines(label), 
                  pandoc.Attr("", refClasses)
                )
                ref:insert(refSpan)
              elseif entry ~= nil then
                if entry.parent ~= nil then
                  local parentType = refType(entry.parent)
                  local parent = crossref.index.entries[entry.parent]
                  ref:extend(refNumberOption(parentType,parent))
                  ref:extend({pandoc.Space(), pandoc.Str("(")})
                  ref:extend(subrefNumber(entry.order))
                  ref:extend({pandoc.Str(")")})
                else
                  ref:extend(refNumberOption(type, entry))
                end
              end
  
                -- link if requested
              if (refHyperlink()) then
                ref = {pandoc.Link(ref, "#" .. label)}
              end
            end
  
            -- add the ref
            refs:extend(ref)
  
          -- no entry for this reference, if it has a valid ref prefix
          -- then yield error text
          elseif tcontains(refTypes, type) then
            warn("Unable to resolve crossref @" .. label)
            local err = pandoc.Strong({ pandoc.Str("?@" .. label) })
            refs:extend({err})
          end
        end
      end

      -- swap citeEl for refs if we found any
      if #refs > 0 then
        return refs
      else
        return citeEl
      end


    end
  }
end

function autoRefLabel(parentId)
  local index = 1
  while true do
    local label = parentId .. "-" .. tostring(index)
    if not crossref.autolabels:includes(label) then
      crossref.autolabels:insert(label)
      return label
    else
      index = index + 1
    end
  end
end

function refLabel(type, inline)
  if inline.text then
    return string.match(inline.text, "^" .. refLabelPattern(type) .. "$")
  else
    return nil
  end
end

function extractRefLabel(type, text)
  return string.match(text, "^(.*)" .. refLabelPattern(type) .. "$")
end

function refLabelPattern(type)
  return "{#(" .. type .. "%-[^ }]+)}"
end

function isValidRefType(type) 
  return tcontains(validRefTypes(), type)
end

function validRefTypes()
  local types = tkeys(theoremTypes)
  table.insert(types, "fig")
  table.insert(types, "tbl")
  table.insert(types, "eq")
  table.insert(types, "lst")
  table.insert(types, "sec")
  return types
end

-- meta.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- inject metadata
function crossrefMetaInject()
  return {
    Meta = function(meta)
      metaInjectLatex(meta, function(inject)
        
        inject(usePackage("caption"))

        inject(
          "\\AtBeginDocument{%\n" ..
          maybeRenewCommand("contentsname", param("toc-title-document", "Table of contents")) ..
          maybeRenewCommand("listfigurename", listOfTitle("lof", "List of Figures")) ..
          maybeRenewCommand("listtablename", listOfTitle("lot", "List of Tables")) ..
          maybeRenewCommand("figurename", titleString("fig", "Figure")) ..
          maybeRenewCommand("tablename", titleString("tbl", "Table")) ..
          "}\n"
        )
      
        if latexListings() then
          inject(
            "\\newcommand*\\listoflistings\\lstlistoflistings\n" ..
            "\\AtBeginDocument{%\n" ..
            "\\renewcommand*\\lstlistlistingname{" .. listOfTitle("lol", "List of Listigs") .. "}\n" ..
            "}\n"
          )
        else
          inject(
            usePackage("float") .. "\n" ..
            "\\floatstyle{ruled}\n" ..
            "\\@ifundefined{c@chapter}{\\newfloat{codelisting}{h}{lop}}{\\newfloat{codelisting}{h}{lop}[chapter]}\n" ..
            "\\floatname{codelisting}{" .. titleString("lst", "Listing") .. "}\n"
          )

          inject(
            "\\newcommand*\\listoflistings{\\listof{codelisting}{" .. listOfTitle("lol", "List of Listings") .. "}}\n"
          )
        end
        
        local theoremIncludes = theoremLatexIncludes()
        if theoremIncludes then
          inject(theoremIncludes)
        end
      end)
      
      return meta
    end
  }
end

function maybeRenewCommand(command, arg) 
  local commandWithArg = command .. "{" .. arg .. "}"
  return "\\ifdefined\\" .. command .. "\n  " .. "\\renewcommand*\\" .. commandWithArg .. "\n\\else\n  " .. "\\newcommand\\" .. commandWithArg .. "\n\\fi\n"
end


-- latex 'listof' title for type
function listOfTitle(type, default)
  local title = crossrefOption(type .. "-title")
  if title then
    return pandoc.utils.stringify(title)
  else
    return param("crossref-" .. type .. "-title", default)
  end
end
-- format.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function title(type, default)
  default = param("crossref-" .. type .. "-title", default)
  return crossrefOption(type .. "-title", stringToInlines(default))
end

function envTitle(type, default)
  return param("environment-" .. type .. "-title", default)
end

function titleString(type, default)
  return pandoc.utils.stringify(title(type, default))
end

function titlePrefix(type, default, order)
  local prefix = title(type, default)
  table.insert(prefix, nbspString())
  tappend(prefix, numberOption(type, order))
  tappend(prefix, titleDelim())
  table.insert(prefix, pandoc.Space())
  return prefix
end

function titleDelim()
  return crossrefOption("title-delim", stringToInlines(":"))
end

function captionSubfig()
  return crossrefOption("caption-subfig", true)
end

function captionCollectedDelim()
  return crossrefOption("caption-collected-delim", stringToInlines(",\u{a0}"))
end

function captionCollectedLabelSep()
  return crossrefOption("caption-collected-label-sep", stringToInlines("\u{a0}—\u{a0}"))
end

function subrefNumber(order)
  return numberOption("subref", order,  {pandoc.Str("alpha"),pandoc.Space(),pandoc.Str("a")})
end

function prependSubrefNumber(captionContent, order)
  if not _quarto.format.isLatexOutput() and not _quarto.format.isAsciiDocOutput() then
    if #inlinesToString(captionContent) > 0 then
      tprepend(captionContent, { pandoc.Space() })
    end
    tprepend(captionContent, { pandoc.Str(")") })
    tprepend(captionContent, subrefNumber(order))
    captionContent:insert(1, pandoc.Str("("))
  end
end

function refPrefix(type, upper)
  local opt = type .. "-prefix"
  local default = stringToInlines(param("crossref-" .. type .. "-prefix", type .. "."))
  local prefix = crossrefOption(opt, default)
  if upper then
    local el = pandoc.Plain(prefix)
    local firstStr = true
    el = _quarto.ast.walk(el, {
      Str = function(str)
        if firstStr then
          local strText = pandoc.text.upper(pandoc.text.sub(str.text, 1, 1)) .. pandoc.text.sub(str.text, 2, -1)
          str = pandoc.Str(strText)
          firstStr = false
        end
        return str
      end
    })
    prefix = el.content
  end
  return prefix
end

function refDelim()
  return crossrefOption("ref-delim", stringToInlines(","))
end

function refHyperlink()
  return crossrefOption("ref-hyperlink", true)
end

function refNumberOption(type, entry)

  -- for sections just return the section levels
  if type == "sec" then
    local num = nil
    if entry.appendix then
      num = string.char(64 + entry.order.section[1] - crossref.startAppendix + 1)
    elseif crossrefOption("chapters", false) then
      num = tostring(entry.order.section[1])
    end
    return stringToInlines(sectionNumber(entry.order.section, nil, num))
  end

  -- handle other ref types
  return formatNumberOption(type, entry.order)
end


function numberOption(type, order, default)
  
  -- for sections, just return the section levels (we don't currently
  -- support custom numbering for sections since pandoc is often the
  -- one doing the numbering)
  if type == "sec" then
    return stringToInlines(sectionNumber(order.section))
  end

  -- format
  return formatNumberOption(type, order, default)
end

function formatNumberOption(type, order, default)

  -- alias num and section (set section to nil if we aren't using chapters)
  local num = order.order
  local section = order.section
  if not crossrefOption("chapters", false) then
    section = nil
  elseif section ~= nil and section[1] == 0 then
    section = nil
  elseif crossref.maxHeading ~= 1 then
    section = nil
  end
  
  -- return a pandoc.Str w/ chapter prefix (if any)
  local function resolve(num)
    if section then
      local sectionIndex = section[1]
      if crossrefOption("chapters-alpha", false) then
        sectionIndex = string.char(64 + sectionIndex)
      elseif crossref.startAppendix ~= nil and sectionIndex >= crossref.startAppendix then
        sectionIndex = string.char(64 + sectionIndex - crossref.startAppendix + 1)
      else
        sectionIndex = tostring(sectionIndex)
      end
      num = sectionIndex .. "." .. num
    end
    return { pandoc.Str(num) }
  end
  
  -- Compute option name and default value
  local opt = type .. "-labels"
  if default == nil then
    default = stringToInlines("arabic")
  end

  -- See if there a global label option, if so, use that
  -- if the type specific label isn't specified
  local labelOpt = crossrefOption("labels", default);
  
  -- determine the style
  local styleRaw = crossrefOption(opt, labelOpt)


  local numberStyle = pandoc.utils.stringify(styleRaw)

  -- process the style
  if (numberStyle == "arabic") then
    return resolve(tostring(num))
  elseif (string.match(numberStyle, "^alpha ")) then
    -- permits the user to include the character that they'd like
    -- to start the numbering with (e.g. alpha a vs. alpha A)
    local startIndexChar = string.sub(numberStyle, -1)
    if (startIndexChar == " ") then
      startIndexChar = "a"
    end
    local startIndex = utf8.codepoint(startIndexChar)
    return resolve(string.char(startIndex + num - 1))
  elseif (string.match(numberStyle, "^roman")) then
    -- permits the user to express `roman` or `roman i` or `roman I` to
    -- use lower / uppper case roman numerals
    local lower = false
    if (string.sub(numberStyle, -#"i") == "i") then
      lower = true
    end
    return resolve(toRoman(num, lower))
  else
    -- otherwise treat the value as a list of values to use
    -- to display the numbers
    local entryCount = #styleRaw

    -- select an index based upon the num, wrapping it around
    local entryIndex = (num - 1) % entryCount + 1
    local option = styleRaw[entryIndex]:clone()
    if section then
      tprepend(option, { pandoc.Str(tostring(section[1]) .. ".") })
    end
    quarto.utils.dump(styleRaw)
    print("HERE!", option)
    return { option }
  end

end


function sectionNumber(section, maxLevel, num)

  if num == nil then
    num = ""
    if crossref.maxHeading == 1 then
      num = formatChapterIndex(section[1])
    end
  end

  local endIndex = #section
  if maxLevel then
    endIndex = maxLevel
  end
  local lastIndex = 1
  for i=endIndex,2,-1 do
    if section[i] > 0 then
      lastIndex = i
      break
    end
  end

  for i=2,lastIndex do
    if num ~= '' then
      num = num .. "."
    end
    num = num .. tostring(section[i])
  end

  return num
end

function isChapterRef(section)
  for i=2,#section do
    if section[i] > 0 then
      return false
    end
  end
  return true
end

function formatChapterIndex(index)
  local fileMetadata = currentFileMetadataState()
  if fileMetadata.appendix then
    return string.char(64 + fileMetadata.file.bookItemNumber)
  elseif crossrefOption("chapters-alpha", false) then
    return string.char(64 + index)
  else
    return tostring(index)
  end
end

function toRoman(num, lower)
  local roman = pandoc.utils.to_roman_numeral(num)
  if lower then
    lower = ''
    for i = 1, #roman do
      lower = lower .. string.char(utf8.codepoint(string.sub(roman,i,i)) + 32)
    end
    return lower
  else
    return roman
  end
end
-- options.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- initialize options from 'crossref' metadata value
function initCrossrefOptions()
  return {
    Meta = function(meta)
      crossref.options = readFilterOptions(meta, "crossref")

      -- automatically set maxHeading to 1 if we are in chapters mode, otherwise set to max (7)
      if crossrefOption("chapters", false) then
        crossref.maxHeading = 1
      else
        crossref.maxHeading = 7
      end

    end
  }
end

-- get option value
function crossrefOption(name, default)
  return readOption(crossref.options, name, default)
end



-- bibliography-formats.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function bibliographyFormats()
  return  {
    Pandoc = function(doc)
      if _quarto.format.isBibliographyOutput() then
        doc.meta.references = pandoc.utils.references(doc)
        doc.meta.bibliography = nil
        return doc
      end
    end
  }
end
-- book-links.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function indexBookFileTargets() 
    if not param("single-file-book", false) then
      return {} 
    else 
      return {
        Header = function(el)
        if el.level == 1 then 
          local file = currentFileMetadataState().file
          if file ~= nil then   
            local filename = file.bookItemFile;
            if filename ~= nil and preState.fileSectionIds[filename] == nil then
              preState.fileSectionIds[filename] = el.identifier
            end
          end
        end
      end
    }
  end
end

function resolveBookFileTargets() 
  if not param("single-file-book", false) then
    return {} 
  else
    return {
      Link = function(el)
        local linkTarget = el.target
        -- if this is a local path
        if isRelativeRef(linkTarget) then
          local file = currentFileMetadataState().file
  
          -- normalize the linkTarget (collapsing any '..')
          if #linkTarget > 0 then
            local fullPath = linkTarget
            if file ~= nil and file.resourceDir ~= nil then
              fullPath = pandoc.path.join({file.resourceDir, linkTarget})
            end
            linkTarget = pandoc.path.normalize(flatten(fullPath));
          end
          
          -- resolve the path
          local hashPos = string.find(linkTarget, '#')
          if hashPos ~= nil then
            -- deal with a link that includes a hash (just remove the prefix)
            local target = string.sub(linkTarget, hashPos, #linkTarget)
            el.target = target
          else
            -- Deal with bare file links
            -- escape windows paths if present
            package.config:sub(1,1)
            
            -- Paths are always using '/' separator (even on windows)
            linkTarget = linkTarget:gsub("\\", "/")
            local sectionId = preState.fileSectionIds[linkTarget];
            if sectionId ~= nil then
              el.target = '#' .. sectionId
            end
          end
        end
        return el
      end 
    }  
  end
end

function flatten(targetPath) 
  local pathParts = pandoc.path.split(targetPath)
  local resolvedPath = pandoc.List()

  for _, part in ipairs(pathParts) do 
    if part == '..' then
      table.remove(resolvedPath)
    else
      resolvedPath:insert(part)
    end
  end
  return pandoc.path.join(resolvedPath)
end
-- book-numbering.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function bookNumbering() 
  return {
    Header = function(el)
      local file = currentFileMetadataState().file
      if file ~= nil then
        local bookItemType = file.bookItemType
        local bookItemDepth = file.bookItemDepth
        if bookItemType ~= nil then
          -- if we are in an unnumbered chapter then add unnumbered class
          if bookItemType == "chapter" and file.bookItemNumber == nil then
            el.attr.classes:insert('unnumbered')
          end

          -- handle latex "part" and "appendix" headers
          if el.level == 1 and _quarto.format.isLatexOutput() then
            if bookItemType == "part" then
              local partPara = pandoc.Para({
                pandoc.RawInline('latex', '\\part{')
              })
              tappend(partPara.content, el.content)
              partPara.content:insert( pandoc.RawInline('latex', '}'))
              return partPara  
            elseif bookItemType == "appendix" then
              local appendixPara = pandoc.Para({
                pandoc.RawInline('latex', '\\cleardoublepage\n\\phantomsection\n\\addcontentsline{toc}{part}{')
              })
              tappend(appendixPara.content, el.content)
              appendixPara.content:insert(pandoc.RawInline('latex', '}\n\\appendix'))
              return appendixPara
            elseif bookItemType == "chapter" and bookItemDepth == 0 then
              preState.usingBookmark = true
              local bookmarkReset = pandoc.Div({
                pandoc.RawInline('latex', '\\bookmarksetup{startatroot}\n'),
                el
              })
              return bookmarkReset
            end
          end

          -- mark appendix chapters for epub
          if el.level == 1 and _quarto.format.isEpubOutput() then
            if file.appendix == true and bookItemType == "chapter" then
              el.attr.attributes["epub:type"] = "appendix"
            end
          end

          -- part cover pages have unnumbered headings
          if (bookItemType == "part") then
            el.attr.classes:insert("unnumbered")
          end

          -- return potentially modified heading el
          return el
        end
      end
    end
  }
end
-- callout.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function calloutType(div)
  for _, class in ipairs(div.attr.classes) do
    if isCallout(class) then 
      local type = class:match("^callout%-(.*)")
      if type == nil then
        type = "none"
      end
      return type
    end
  end
  return nil
end

_quarto.ast.add_handler({
  -- use either string or array of strings
  class_name = {"callout", "callout-note", "callout-warning", "callout-important", "callout-caution", "callout-tip" },

  -- the name of the ast node, used as a key in extended ast filter tables
  ast_name = "Callout",

  -- callouts will be rendered as blocks
  kind = "Block",

  -- a function that takes the div node as supplied in user markdown
  -- and returns the custom node
  parse = function(div)
    preState.hasCallouts = true
    local title = markdownToInlines(div.attr.attributes["title"])
    if not title or #title == 0 then
      title = resolveHeadingCaption(div)
    end
    local old_attr = div.attr
    local appearanceRaw = div.attr.attributes["appearance"]
    local icon = div.attr.attributes["icon"]
    local collapse = div.attr.attributes["collapse"]
    div.attr.attributes["appearance"] = nil
    div.attr.attributes["collapse"] = nil
    div.attr.attributes["icon"] = nil
    local callout_type = calloutType(div)
    div.attr.classes = div.attr.classes:filter(function(class) return not isCallout(class) end)    
    return quarto.Callout({
      appearance = appearanceRaw,
      title = title,
      collapse = collapse,
      content = div.content,
      icon = icon,
      type = callout_type,
      attr = old_attr,
    })
  end,

  -- a function that renders the extendedNode into output
  render = function(node)
    if _quarto.format.isHtmlOutput() and hasBootstrap() then
      local result = calloutDiv(node)
      return result
    elseif _quarto.format.isLatexOutput() then
      return calloutLatex(node)
    elseif _quarto.format.isDocxOutput() then
      return calloutDocx(node)
    elseif _quarto.format.isJatsOutput() then
      return jatsCallout(node)
    elseif _quarto.format.isEpubOutput() or _quarto.format.isRevealJsOutput() then
      return epubCallout(node)
    else
      return simpleCallout(node)
    end
  end,

  -- a function that takes the extended node and
  -- returns a table with table-valued attributes
  -- that represent inner content that should
  -- be visible to filters.
  inner_content = function(extended_node)
    return {
      content = extended_node.content,
      title = extended_node.title
    }
  end,

  -- a function that updates the extended node
  -- with new inner content (as returned by filters)
  -- table keys are a subset of those returned by inner_content
  -- and represent changed values that need to be updated.    
  set_inner_content = function(extended_node, values)
    if values.title then
      extended_node.title = values.title
    end
    if values.content then
      extended_node.content = values.content
    end
  end,

  constructor = function(tbl)
    preState.hasCallouts = true

    local t = tbl.type
    local iconDefault = true
    local appearanceDefault = nil
    if t == "none" then
      iconDefault = false
      appearanceDefault = "simple"
    end
    local appearanceRaw = tbl.appearance
    if appearanceRaw == nil then
      appearanceRaw = option("callout-appearance", appearanceDefault)
    end

    local icon = tbl.icon
    if icon == nil then
      icon = option("callout-icon", iconDefault)
    elseif icon == "false" then
      icon = false
    end

    local appearance = nameForCalloutStyle(appearanceRaw);
    if appearance == "minimal" then
      icon = false
      appearance = "simple"
    end
    local content = pandoc.Blocks({})
    content:extend(tbl.content)
    local title = tbl.title
    if type(title) == "string" then
      title = pandoc.Str(title)
    end
    return {
      title = title,
      collapse = tbl.collapse,
      content = content,
      appearance = appearance,
      icon = icon,
      type = t,
      attr = tbl.attr,
    }
  end
})

local calloutidx = 1

function callout() 
  return {
  
    -- Insert paragraphs between consecutive callouts or tables for docx
    Blocks = function(blocks)
      if _quarto.format.isDocxOutput() then
        local lastWasCallout = false
        local lastWasTableOrFigure = false
        local newBlocks = pandoc.List()
        for i,el in ipairs(blocks) do 
          -- determine what this block is
          local isCallout = el.t == "Callout"
          local isTableOrFigure = el.t == "Table" or isFigureDiv(el) or (discoverFigure(el, true) ~= nil)
          local isCodeBlock = el.t == "CodeBlock"

          -- Determine whether this is a code cell that outputs a table
          local isCodeCell = el.t == "Div" and el.attr.classes:find_if(isCodeCell)
          if isCodeCell and (isCodeCellTable(el) or isCodeCellFigure(el)) then 
            isTableOrFigure = true
          end
          
          -- insert spacer if appropriate
          local insertSpacer = false
          if isCallout and (lastWasCallout or lastWasTableOrFigure) then
            insertSpacer = true
          end
          if isCodeBlock and lastWasCallout then
            insertSpacer = true
          end
          if isTableOrFigure and lastWasTableOrFigure then
            insertSpacer = true
          end

          if insertSpacer then
            newBlocks:insert(pandoc.Para(stringToInlines(" ")))
          end

          -- always insert
          newBlocks:insert(el)

          -- record last state
          lastWasCallout = isCallout
          lastWasTableOrFigure = isTableOrFigure
        end

        if #newBlocks > #blocks then
          return newBlocks
        else
          return nil
        end
      end
    end

  }
end

function isCallout(class)
  return class == 'callout' or class:match("^callout%-")
end

function isDocxCallout(class)
  return class == "docx-callout"
end

function isCodeCell(class)
  return class == "cell"
end

function isCodeCellDisplay(class)
  return class == "cell-output-display"
end

-- Attempts to detect whether this element is a code cell
-- whose output is a table
function isCodeCellTable(el) 
  local isTable = false
  _quarto.ast.walk(el, {
    Div = function(div)
      if div.attr.classes:find_if(isCodeCellDisplay) then
        _quarto.ast.walk(div, {
          Table = function(tbl)
            isTable = true
          end
        })
      end
    end
  })
  return isTable
end

function isCodeCellFigure(el)
  local isFigure = false
  _quarto.ast.walk(el, {
    Div = function(div)
      if div.attr.classes:find_if(isCodeCellDisplay) then
        if (isFigureDiv(div)) then
          isFigure = true
        elseif div.content and #div.content > 0 then 
          isFigure = discoverFigure(div.content[1], true) ~= nil
        end
      end
    end
  })
  return isFigure
end

local kCalloutAppearanceDefault = "default"
local kCalloutDefaultSimple = "simple"
local kCalloutDefaultMinimal = "minimal"

-- an HTML callout div
function calloutDiv(node)
  -- the first heading is the title
  local div = pandoc.Div({})
  div.content:extend(node.content)
  local title = node.title
  local type = node.type
  local calloutAppearance = node.appearance
  local icon = node.icon
  local collapse = node.collapse

  if calloutAppearance == kCalloutAppearanceDefault and title == nil then
    title = displayName(type)
  end

  -- Make an outer card div and transfer classes and id
  local calloutDiv = pandoc.Div({})
  calloutDiv.attr = (node.attr or pandoc.Attr()):clone()
  div.attr.classes = pandoc.List() 
  div.attr.classes:insert("callout-body-container")

  -- add card attribute
  calloutDiv.attr.classes:insert("callout")
  calloutDiv.attr.classes:insert("callout-style-" .. calloutAppearance)
  if node.type ~= nil then
    calloutDiv.attr.classes:insert("callout-" .. node.type)
  end

  -- the image placeholder
  local noicon = ""

  -- Check to see whether this is a recognized type
  if icon == false or not isBuiltInType(type) or type == nil then
    noicon = " no-icon"
    calloutDiv.attr.classes:insert("no-icon")
  end
  local imgPlaceholder = pandoc.Plain({pandoc.RawInline("html", "<i class='callout-icon" .. noicon .. "'></i>")});       
  local imgDiv = pandoc.Div({imgPlaceholder}, pandoc.Attr("", {"callout-icon-container"}));

  -- show a titled callout
  if title ~= nil then

    -- mark the callout as being titleed
    calloutDiv.attr.classes:insert("callout-titled")

    -- create a unique id for the callout
    local calloutid = "callout-" .. calloutidx
    calloutidx = calloutidx + 1

    -- create the header to contain the title
    -- title should expand to fill its space
    local titleDiv = pandoc.Div(pandoc.Plain(title), pandoc.Attr("", {"callout-title-container", "flex-fill"}))
    local headerDiv = pandoc.Div({imgDiv, titleDiv}, pandoc.Attr("", {"callout-header", "d-flex", "align-content-center"}))
    local bodyDiv = div
    bodyDiv.attr.classes:insert("callout-body")

    if collapse ~= nil then 

      -- collapse default value     
      local expandedAttrVal= "true"
      if collapse == "true" or collapse == true then
        expandedAttrVal = "false"
      end

      -- create the collapse button
      local btnClasses = "callout-btn-toggle d-inline-block border-0 py-1 ps-1 pe-0 float-end"
      local btnIcon = "<i class='callout-toggle'></i>"
      local toggleButton = pandoc.RawInline("html", "<div class='" .. btnClasses .. "'>" .. btnIcon .. "</div>")
      headerDiv.content:insert(pandoc.Plain(toggleButton));

      -- configure the header div for collapse
      local bsTargetClz = calloutid .. "-contents"
      headerDiv.attr.attributes["bs-toggle"] = "collapse"
      headerDiv.attr.attributes["bs-target"] = "." .. bsTargetClz
      headerDiv.attr.attributes["aria-controls"] = calloutid
      headerDiv.attr.attributes["aria-expanded"] = expandedAttrVal
      headerDiv.attr.attributes["aria-label"] = 'Toggle callout'

      -- configure the body div for collapse
      local collapseDiv = pandoc.Div({})
      collapseDiv.attr.identifier = calloutid
      collapseDiv.attr.classes:insert(bsTargetClz)
      collapseDiv.attr.classes:insert("callout-collapse")
      collapseDiv.attr.classes:insert("collapse")
      if expandedAttrVal == "true" then
        collapseDiv.attr.classes:insert("show")
      end

      -- add the current body to the collapse div and use the collapse div instead
      collapseDiv.content:insert(bodyDiv)
      bodyDiv = collapseDiv
    end

    -- add the header and body to the div
    calloutDiv.content:insert(headerDiv)
    calloutDiv.content:insert(bodyDiv)
  else 
    -- show an untitleed callout
  
    -- create a card body
    local containerDiv = pandoc.Div({imgDiv, div}, pandoc.Attr("", {"callout-body"}))
    containerDiv.attr.classes:insert("d-flex")

    -- add the container to the callout card
    calloutDiv.content:insert(containerDiv)
  end
  
  return calloutDiv
end

-- Latex callout
function calloutLatex(node)
  -- read and clear attributes
  local title = node.title
  local type = node.type
  local calloutAppearance = node.appearance
  local icon = node.icon
  

  -- Discover notes in the callout and pull the contents out
  -- replacing with a footnote mark. This is required because
  -- if the footnote stays in the callout, the footnote text
  -- will not appear at the bottom of the document but will instead
  -- appear in the callout itself (at the bottom)
  -- 
  -- Also note whether the footnotes contain codeblocks, which
  -- require special handling
  local hasVerbatimInNotes = false
  local noteContents = {}
  local nodeContent = node.content:walk({
    Note = function(el)
      tappend(noteContents, {el.content})
      el.content:walk({
        CodeBlock = function(el)
          hasVerbatimInNotes = true
        end
      })
      return pandoc.RawInline('latex', '\\footnotemark{}')
    end
  })

  -- generate the callout box
  local callout
  if calloutAppearance == kCalloutAppearanceDefault then
    if title == nil then
      title = displayName(type)
    else
      title = pandoc.write(pandoc.Pandoc(pandoc.Plain(title)), 'latex')
    end
    callout = latexCalloutBoxDefault(title, type, icon)
  else
    callout = latexCalloutBoxSimple(title, type, icon)
  end
  local beginEnvironment = callout.beginInlines
  local endEnvironment = callout.endInlines
  local calloutContents = callout.contents
  if calloutContents == nil then
    calloutContents = pandoc.List({})
  end

  tappend(calloutContents, nodeContent)
  
  if calloutContents[1] ~= nil and calloutContents[1].t == "Para" and calloutContents[#calloutContents].t == "Para" then
    tprepend(calloutContents, { pandoc.Plain(beginEnvironment) })
    tappend(calloutContents, { pandoc.Plain(endEnvironment) })
  else
    tprepend(calloutContents, { pandoc.Para(beginEnvironment) })
    tappend(calloutContents, { pandoc.Para(endEnvironment) })
  end

  
  -- For any footnote content that was pulled out, append a footnotetext
  -- that include the contents
  for _i, v in ipairs(noteContents) do
    -- If there are paragraphs, just attach to them when possible
    if v[1].t == "Para" then
      table.insert(v[1].content, 1, pandoc.RawInline('latex', '\\footnotetext{'))
    else
      v:insert(1, pandoc.RawInline('latex', '\\footnotetext{'))
    end
      
    if v[#v].t == "Para" then
      table.insert(v[#v].content, pandoc.RawInline('latex', '}'))
    else
      v:extend({pandoc.RawInline('latex', '}')})
    end
    tappend(calloutContents, v)
  end 

  -- Enable fancyvrb if verbatim appears in the footnotes
  if hasVerbatimInNotes then
    quarto.doc.use_latex_package('fancyvrb')
    quarto.doc.include_text('in-header', '\\VerbatimFootnotes')
  end
  

  return pandoc.Div(calloutContents)
end

function latexCalloutBoxDefault(title, type, icon) 

  -- callout dimensions
  local leftBorderWidth = '.75mm'
  local borderWidth = '.15mm'
  local borderRadius = '.35mm'
  local leftPad = '2mm'
  local color = latexColorForType(type)
  local frameColor = latexFrameColorForType(type)

  local iconForType = iconForType(type)

  -- generate options
  local options = {
    breakable = "",
    colframe = frameColor,
    colbacktitle = color ..'!10!white',
    coltitle = 'black',
    colback = 'white',
    opacityback = 0,
    opacitybacktitle =  0.6,
    left = leftPad,
    leftrule = leftBorderWidth,
    toprule = borderWidth, 
    bottomrule = borderWidth,
    rightrule = borderWidth,
    arc = borderRadius,
    title = '{' .. title .. '}',
    titlerule = '0mm',
    toptitle = '1mm',
    bottomtitle = '1mm',
  }

  if icon ~= false and iconForType ~= nil then
    options.title = '\\textcolor{' .. color .. '}{\\' .. iconForType .. '}\\hspace{0.5em}' ..  options.title
  end

  -- the core latex for the box
  local beginInlines = { pandoc.RawInline('latex', '\\begin{tcolorbox}[enhanced jigsaw, ' .. tColorOptions(options) .. ']\n') }
  local endInlines = { pandoc.RawInline('latex', '\n\\end{tcolorbox}') }

  -- Add the titles and contents
  local calloutContents = pandoc.List({});

  -- the inlines
  return { 
    contents = calloutContents,
    beginInlines = beginInlines, 
    endInlines = endInlines
  }

end

-- create the tcolorBox
function latexCalloutBoxSimple(title, type, icon)

  -- callout dimensions
  local leftBorderWidth = '.75mm'
  local borderWidth = '.15mm'
  local borderRadius = '.35mm'
  local leftPad = '2mm'
  local color = latexColorForType(type)
  local colorFrame = latexFrameColorForType(type)

  -- generate options
  local options = {
    breakable = "",
    colframe = colorFrame,
    colback = 'white',
    opacityback = 0,
    left = leftPad,
    leftrule = leftBorderWidth,
    toprule = borderWidth, 
    bottomrule = borderWidth,
    rightrule = borderWidth,
    arc = borderRadius,
  }

  -- the core latex for the box
  local beginInlines = { pandoc.RawInline('latex', '\\begin{tcolorbox}[enhanced jigsaw, ' .. tColorOptions(options) .. ']\n') }
  local endInlines = { pandoc.RawInline('latex', '\n\\end{tcolorbox}') }

  -- generate the icon and use a minipage to position it
  local iconForCat = iconForType(type)
  if icon ~= false and iconForCat ~= nil then
    local iconName = '\\' .. iconForCat
    local iconColSize = '5.5mm'

    -- add an icon to the begin
    local iconTex = '\\begin{minipage}[t]{' .. iconColSize .. '}\n\\textcolor{' .. color .. '}{' .. iconName .. '}\n\\end{minipage}%\n\\begin{minipage}[t]{\\textwidth - ' .. iconColSize .. '}\n'
    tappend(beginInlines, {pandoc.RawInline('latex',  iconTex)})

    -- close the icon
    tprepend(endInlines, {pandoc.RawInline('latex', '\\end{minipage}%')});
  end

  -- Add the titles and contents
  local calloutContents = pandoc.List({});
  if title ~= nil then 
    tprepend(title, {pandoc.RawInline('latex', '\\textbf{')})
    tappend(title, {pandoc.RawInline('latex', '}\\vspace{2mm}')})
    calloutContents:insert(pandoc.Para(title))
  end

  -- the inlines
  return { 
    contents = calloutContents,
    beginInlines = beginInlines, 
    endInlines = endInlines
  }
end

function calloutDocx(node)
  local type = node.type
  local appearance = node.appearance
  local hasIcon = node.icon 

  if appearance == kCalloutAppearanceDefault then
    return calloutDocxDefault(node, type, hasIcon)
  else
    return calloutDocxSimple(node, type, hasIcon)
  end
end

function calloutDocxDefault(node, type, hasIcon)
  local title = node.title
  local color = htmlColorForType(type)
  local backgroundColor = htmlBackgroundColorForType(type)

  local tablePrefix = [[
    <w:tbl>
    <w:tblPr>
      <w:tblStyle w:val="Table" />
      <w:tblLook w:firstRow="0" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="0" w:val="0000" />
      <w:tblBorders>  
        <w:left w:val="single" w:sz="24" w:space="0" w:color="$color"/>  
        <w:right w:val="single" w:sz="4" w:space="0" w:color="$color"/>  
        <w:top w:val="single" w:sz="4" w:space="0" w:color="$color"/>  
        <w:bottom w:val="single" w:sz="4" w:space="0" w:color="$color"/>  
      </w:tblBorders> 
      <w:tblCellMar>
        <w:left w:w="144" w:type="dxa" />
        <w:right w:w="144" w:type="dxa" />
      </w:tblCellMar>
      <w:tblInd w:w="164" w:type="dxa" />
      <w:tblW w:type="pct" w:w="100%"/>
    </w:tblPr>
    <w:tr>
      <w:trPr>
        <w:cantSplit/>
      </w:trPr>
      <w:tc>
        <w:tcPr>
          <w:shd w:color="auto" w:fill="$background" w:val="clear"/>
          <w:tcMar>
            <w:top w:w="92" w:type="dxa" />
            <w:bottom w:w="92" w:type="dxa" />
          </w:tcMar>
        </w:tcPr>
  ]]
  local calloutContents = pandoc.List({
    pandoc.RawBlock("openxml", tablePrefix:gsub('$background', backgroundColor):gsub('$color', color)),
  })

  -- Create a title if there isn't already one
  if title == nil then
    title = pandoc.List({pandoc.Str(displayName(type))})
  end

  -- add the image to the title, if needed
  local calloutImage = docxCalloutImage(type);
  if hasIcon and calloutImage ~= nil then
    -- Create a paragraph with the icon, spaces, and text
    local image_title = pandoc.List({
        pandoc.RawInline("openxml", '<w:pPr>\n<w:spacing w:before="0" w:after="0" />\n<w:textAlignment w:val="center"/>\n</w:pPr>'), 
        calloutImage,
        pandoc.Space(), 
        pandoc.Space()})
    tappend(image_title, title)
    calloutContents:insert(pandoc.Para(image_title))
  else
    local titleRaw = openXmlPara(pandoc.Para(title), 'w:before="16" w:after="16"')
    calloutContents:insert(titleRaw)  
  end

  
  -- end the title row and start the body row
  local tableMiddle = [[
      </w:tc>
    </w:tr>
    <w:tr>
      <w:trPr>
        <w:cantSplit/>
      </w:trPr>
      <w:tc> 
      <w:tcPr>
        <w:tcMar>
          <w:top w:w="108" w:type="dxa" />
          <w:bottom w:w="108" w:type="dxa" />
        </w:tcMar>
      </w:tcPr>

  ]]
  calloutContents:insert(pandoc.Div(pandoc.RawBlock("openxml", tableMiddle)))  

  -- the main contents of the callout
  local contents = node.content

  -- ensure there are no nested callouts
  if contents:find_if(function(el) 
    return el.t == "Div" and el.attr.classes:find_if(isDocxCallout) ~= nil 
  end) ~= nil then
    fail("Found a nested callout in the document. Please fix this issue and try again.")
  end
  
  -- remove padding from existing content and add it
  removeParagraphPadding(contents)
  tappend(calloutContents, contents)

  -- close the table
  local suffix = pandoc.List({pandoc.RawBlock("openxml", [[
    </w:tc>
    </w:tr>
  </w:tbl>
  ]])})
  tappend(calloutContents, suffix)

  -- return the callout
  local callout = pandoc.Div(calloutContents, pandoc.Attr("", {"docx-callout"}))
  return callout
end


function calloutDocxSimple(node, type, hasIcon) 
  local color = htmlColorForType(type)
  local title = node.title

  local tablePrefix = [[
    <w:tbl>
    <w:tblPr>
      <w:tblStyle w:val="Table" />
      <w:tblLook w:firstRow="0" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="0" w:val="0000" />
      <w:tblBorders>  
        <w:left w:val="single" w:sz="24" w:space="0" w:color="$color"/>  
      </w:tblBorders> 
      <w:tblCellMar>
        <w:left w:w="0" w:type="dxa" />
        <w:right w:w="0" w:type="dxa" />
      </w:tblCellMar>
      <w:tblInd w:w="164" w:type="dxa" />
    </w:tblPr>
    <w:tr>
      <w:trPr>
        <w:cantSplit/>
      </w:trPr>
      <w:tc>
  ]]

  local prefix = pandoc.List({
    pandoc.RawBlock("openxml", tablePrefix:gsub('$color', color)),
  })

  local calloutImage = docxCalloutImage(type)
  if hasIcon and calloutImage ~= nil then
    local imagePara = pandoc.Para({
      pandoc.RawInline("openxml", '<w:pPr>\n<w:spacing w:before="0" w:after="8" />\n<w:jc w:val="center" />\n</w:pPr>'), calloutImage})
    prefix:insert(pandoc.RawBlock("openxml", '<w:tcPr><w:tcMar><w:left w:w="144" w:type="dxa" /><w:right w:w="144" w:type="dxa" /></w:tcMar></w:tcPr>'))
    prefix:insert(imagePara)
    prefix:insert(pandoc.RawBlock("openxml",  "</w:tc>\n<w:tc>"))
  else     
    prefix:insert(pandoc.RawBlock("openxml", '<w:tcPr><w:tcMar><w:left w:w="144" w:type="dxa" /></w:tcMar></w:tcPr>'))
  end

  local suffix = pandoc.List({pandoc.RawBlock("openxml", [[
    </w:tc>
    </w:tr>
  </w:tbl>
  ]])})

  local calloutContents = pandoc.List({})
  tappend(calloutContents, prefix)

  -- deal with the title, if present
  if title ~= nil then
    local titlePara = pandoc.Para(pandoc.Strong(title))
    calloutContents:insert(openXmlPara(titlePara, 'w:before="16" w:after="64"'))
  end
  
  -- convert to open xml paragraph
  local contents = pandoc.List({}) -- use as pandoc.List() for find_if
  contents:extend(node.content)
  removeParagraphPadding(contents)
  
  -- ensure there are no nested callouts
  if contents:find_if(function(el) 
    return el.t == "Div" and el.attr.classes:find_if(isDocxCallout) ~= nil 
  end) ~= nil then
    fail("Found a nested callout in the document. Please fix this issue and try again.")
  end

  tappend(calloutContents, contents)
  tappend(calloutContents, suffix)

  local callout = pandoc.Div(calloutContents, pandoc.Attr("", {"docx-callout"}))
  return callout
end

function epubCallout(node)
  local title = node.title
  local type = node.type
  local calloutAppearance = node.appearance
  local hasIcon = node.icon

  if calloutAppearance == kCalloutAppearanceDefault and title == nil then
    title = displayName(type)
  end
  
  -- the body of the callout
  local calloutBody = pandoc.Div({}, pandoc.Attr("", {"callout-body"}))

  local imgPlaceholder = pandoc.Plain({pandoc.RawInline("html", "<i class='callout-icon'></i>")});       
  local imgDiv = pandoc.Div({imgPlaceholder}, pandoc.Attr("", {"callout-icon-container"}));

  -- title
  if title ~= nil then
    local callout_title = pandoc.Div({}, pandoc.Attr("", {"callout-title"}))
    if hasIcon then
      callout_title.content:insert(imgDiv)
    end
    callout_title.content:insert(pandoc.Para(pandoc.Strong(title)))
    calloutBody.content:insert(callout_title)
  else 
    if hasIcon then
      calloutBody.content:insert(imgDiv)
    end
  end

  -- contents 
  local calloutContents = pandoc.Div(node.content, pandoc.Attr("", {"callout-content"}))
  calloutBody.content:insert(calloutContents)

  -- set attributes (including hiding icon)
  local attributes = pandoc.List({"callout"})
  if type ~= nil then
    attributes:insert("callout-" .. type)
  end

  if hasIcon == false then
    attributes:insert("no-icon")
  end
  if title ~= nil then
    attributes:insert("callout-titled")
  end
  attributes:insert("callout-style-" .. calloutAppearance)

  return pandoc.Div({calloutBody}, pandoc.Attr(node.id or "", attributes))
end

function jatsCallout(node)
  local contents = resolveCalloutContents(node, true)

  local boxedStart = '<boxed-text>'
  if node.id and node.id ~= "" then
    boxedStart = "<boxed-text id='" .. node.id .. "'>"
  end
  contents:insert(1, pandoc.RawBlock('jats', boxedStart))
  contents:insert(pandoc.RawBlock('jats', '</boxed-text>'))
  return contents
end

function simpleCallout(node) 
  local contents = resolveCalloutContents(node, true)
  local callout = pandoc.BlockQuote(contents)
  return pandoc.Div(callout, pandoc.Attr(node.id or ""))
end

function resolveCalloutContents(node, require_title)
  local title = node.title
  local type = node.type
  
  local contents = pandoc.List({})
    
  -- Add the titles and contents
  -- class_name 
  if title == nil and require_title then 
    ---@diagnostic disable-next-line: need-check-nil
    title = stringToInlines(type:sub(1,1):upper()..type:sub(2))
  end
  
  -- raw paragraph with styles (left border, colored)
  if title ~= nil then
    contents:insert(pandoc.Para(pandoc.Strong(title)))
  end
  tappend(contents, node.content)

  return contents
end

function removeParagraphPadding(contents) 
  if #contents > 0 then

    if #contents == 1 then
      if contents[1].t == "Para" then
        contents[1] = openXmlPara(contents[1], 'w:before="16" w:after="16"')
      end  
    else
      if contents[1].t == "Para" then 
        contents[1] = openXmlPara(contents[1], 'w:before="16"')
      end

      if contents[#contents].t == "Para" then 
        contents[#contents] = openXmlPara(contents[#contents], 'w:after="16"')
      end
    end
  end
end

function openXmlPara(para, spacing)
  local xmlPara = pandoc.Para({
    pandoc.RawInline("openxml", "<w:pPr>\n<w:spacing " .. spacing .. "/>\n</w:pPr>")
  })
  tappend(xmlPara.content, para.content)
  return xmlPara
end

function nameForCalloutStyle(calloutType) 
  if calloutType == nil then
    return "default"
  else 
    local name = pandoc.utils.stringify(calloutType);

    if name:lower() == "minimal" then
      return "minimal"
    elseif name:lower() == "simple" then
      return "simple"
    else
      return "default"
    end
  end
end

local kDefaultDpi = 96
function docxCalloutImage(type)

  -- If the DPI has been changed, we need to scale the callout icon
  local dpi = pandoc.WriterOptions(PANDOC_WRITER_OPTIONS)['dpi']
  local scaleFactor = 1
  if dpi ~= nil then
    scaleFactor = dpi / kDefaultDpi
  end

  -- try to form the svg name
  local svg = nil
  if type ~= nil then
    svg = param("icon-" .. type, nil)
  end

  -- lookup the image
  if svg ~= nil then
    local img = pandoc.Image({}, svg, '', {[kProjectResolverIgnore]="true"})
    img.attr.attributes["width"] = tostring(16 * scaleFactor)
    img.attr.attributes["height"] = tostring(16 * scaleFactor)
    return img
  else
    return nil
  end
end

local callout_attrs = {
  note = {
    color = kColorNote,
    background_color = kBackgroundColorNote,
    latex_color = "quarto-callout-note-color",
    latex_frame_color = "quarto-callout-note-color-frame",
    fa_icon = "faInfo"
  },
  warning = {
    color = kColorWarning,
    background_color = kBackgroundColorWarning,
    latex_color = "quarto-callout-warning-color",
    latex_frame_color = "quarto-callout-warning-color-frame",
    fa_icon = "faExclamationTriangle"
  },
  important = {
    color = kColorImportant,
    background_color = kBackgroundColorImportant,
    latex_color = "quarto-callout-important-color",
    latex_frame_color = "quarto-callout-important-color-frame",
    fa_icon = "faExclamation"
  },
  caution = {
    color = kColorCaution,
    background_color = kBackgroundColorCaution,
    latex_color = "quarto-callout-caution-color",
    latex_frame_color = "quarto-callout-caution-color-frame",
    fa_icon = "faFire"
  },
  tip = {
    color = kColorTip,
    background_color = kBackgroundColorTip,
    latex_color = "quarto-callout-tip-color",
    latex_frame_color = "quarto-callout-tip-color-frame",
    fa_icon = "faLightbulb"
  },

  __other = {
    color = kColorUnknown,
    background_color = kColorUnknown,
    latex_color = "quarto-callout-color",
    latex_color_frame = "quarto-callout-color-frame",
    fa_icon = nil
  }
}

setmetatable(callout_attrs, {
  __index = function(tbl, key)
    return tbl.__other
  end
})

function htmlColorForType(type) 
  return callout_attrs[type].color
end

function htmlBackgroundColorForType(type)
  return callout_attrs[type].background_color
end

function latexColorForType(type) 
  return callout_attrs[type].latex_color
end

function latexFrameColorForType(type) 
  return callout_attrs[type].latex_frame_color
end

function iconForType(type) 
  return callout_attrs[type].fa_icon
end

function isBuiltInType(type)
  local icon = iconForType(type)
  return icon ~= nil
end

function displayName(type)
  local defaultName = type:sub(1,1):upper()..type:sub(2)
  return param("callout-" .. type .. "-title", defaultName)
end
-- code.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


-- for a given language, the comment character(s)
local kLangCommentChars = {
  r = {"#"},
  python = {"#"},
  julia = {"#"},
  scala = {"//"},
  matlab = {"%"},
  csharp = {"//"},
  fsharp = {"//"},
  c = {"/*", "*/"},
  css = {"/*", "*/"},
  sas = {"*", ";"},
  powershell = {"#"},
  bash = {"#"},
  sql = {"--"},
  mysql = {"--"},
  psql = {"--"},
  lua = {"--"},
  cpp = {"//"},
  cc = {"//"},
  stan = {"#"},
  octave = {"#"},
  fortran = {"!"},
  fortran95 = {"!"},
  awk = {"#"},
  gawk = {"#"},
  stata = {"*"},
  java = {"//"},
  groovy = {"//"},
  sed = {"#"},
  perl = {"#"},
  ruby = {"#"},
  tikz = {"%"},
  js = {"//"},
  d3 = {"//"},
  node = {"//"},
  sass = {"//"},
  scss = {"//"},
  coffee = {"#"},
  go = {"//"},
  asy = {"//"},
  haskell = {"--"},
  dot = {"//"},
  mermaid = {"%%"},
  apl = {"⍝"},
  yaml = {"#"},
  json = {"//"},
  latex = {"%"},
  typescript = {"//"},
  swift = { "//" },
  javascript = { "//"},
  elm = { "#" },
  vhdl = { "--"}

}

local kCodeAnnotationsParam = 'code-annotations'
local kDataCodeCellTarget = 'data-code-cell'
local kDataCodeCellLines = 'data-code-lines'
local kDataCodeCellAnnotation = 'data-code-annotation'
local kDataCodeAnnonationClz = 'code-annotation-code'

local kCodeAnnotationStyleNone = "none"

local kCodeLine = "code-line"
local kCodeLines = "code-lines"

local hasAnnotations = false;

local kCellAnnotationClass = "cell-annotation"


function isAnnotationCell(el) 
  return el and el.t == "Div" and el.attr.classes:includes(kCellAnnotationClass)
end
-- annotations appear at the end of the line and are of the form
-- # <1> 
-- where they start with a comment character valid for that code cell
-- and they contain a number which is the annotation number in the
-- OL that will appear after the annotation


-- This provider will yield functions for a particular language that 
-- can be used to resolve annotation numbers and strip them from source 
-- code
local function annoteProvider(lang) 
  local commentChars = kLangCommentChars[lang]
  if commentChars ~= nil then

    local startComment = patternEscape(commentChars[1])
    local matchExpr = '.*' .. startComment .. '%s*<([0-9]+)>%s*'
    local stripPrefix = '%s*' .. startComment .. '%s*<'
    local stripSuffix = '>%s*'
    if #commentChars == 2 then
      local endComment = patternEscape(commentChars[2])
      matchExpr = matchExpr .. endComment .. '%s*'
      stripSuffix = stripSuffix .. endComment .. '%s*'
    end
    matchExpr = matchExpr .. '$'
    stripSuffix = stripSuffix .. '$'

    local expression = {
        match = matchExpr,
        strip = {
          prefix = stripPrefix,
          suffix = stripSuffix
        },
      }

    return {
      annotationNumber = function(line) 
          local _, _, annoteNumber = string.find(line, expression.match)
          if annoteNumber ~= nil then
            return tonumber(annoteNumber)
          else
            return nil
          end
      end,
      stripAnnotation = function(line, annoteId) 
        return line:gsub(expression.strip.prefix .. annoteId .. expression.strip.suffix, "")
      end,
      replaceAnnotation = function(line, annoteId, replacement)
        return line:gsub(patternEscape(expression.strip.prefix .. annoteId .. expression.strip.suffix), replacement)
      end,
      createComment = function(value) 
        if #commentChars == 0 then
          return value
        else if #commentChars == 1 then
          return commentChars[1] .. ' ' .. value
        else
          return commentChars[1] .. ' '.. value .. ' ' .. commentChars[2]
        end
      end

      end
    }
  else
    return nil
  end
end


local function toAnnoteId(number) 
  return 'annote-' .. tostring(number)
end

local function latexListPlaceholder(number)
  return '5CB6E08D-list-annote-' .. number 
end

local function toLines(s)
  if s:sub(-1)~="\n" then s=s.."\n" end
  return s:gmatch("(.-)\n")
end

-- Finds annotations in a code cell and returns 
-- the annotations as well as a code cell that
-- removes the annotations
local function resolveCellAnnotes(codeBlockEl, processAnnotation) 

  -- collect any annotations on this code cell
  local lang = codeBlockEl.attr.classes[1]  
  local annotationProvider = annoteProvider(lang)
  if annotationProvider ~= nil then
    local annotations = {}
    local code = codeBlockEl.text
    
    local outputs = pandoc.List({})
    local i = 1
    for line in toLines(code) do
  
      -- Look and annotation
      local annoteNumber = annotationProvider.annotationNumber(line)
      
      if annoteNumber then
        -- Capture the annotation number and strip it
        local annoteId = toAnnoteId(annoteNumber)
        local lineNumbers = annotations[annoteId]
        if lineNumbers == nil then
          lineNumbers = pandoc.List({})
        end
        lineNumbers:insert(i)
        annotations[annoteId] = lineNumbers      
        outputs:insert(processAnnotation(line, annoteNumber, annotationProvider))
      else
        outputs:insert(line)
      end
      i = i + 1
    end    

    -- if we capture annotations, then replace the code source
    -- code, stripping annotation comments
    if annotations and next(annotations) ~= nil then
      local outputText = ""
      for i, output in ipairs(outputs) do
        outputText = outputText .. output .. '\n'
      end
      codeBlockEl.text = outputText
      hasAnnotations = true
    end
    return codeBlockEl, annotations 
  elseif lang then
    return codeBlockEl, {}
  end
  
end

local function lineNumberMeta(list) 

  -- accumulates the output string
  local val = ''
  local addLines = function(lines) 
    if val == '' then
      val = lines
    else 
      val = val .. ',' .. lines
    end
  end

  -- writes out either an individual number of a range
  -- of numbers (from pending to current)
  local pending = nil
  local current = nil
  local valuesWritten = 0;
  local writePending = function()
    if pending == current then
      addLines(tostring(current))
      pending = nil
      current = nil
      valuesWritten = valuesWritten + 1 -- one for the pending line number
    else
      addLines(tostring(pending) .. '-' .. tostring(current))
      pending = nil
      current = nil
      valuesWritten = valuesWritten + 2 -- one for pending, one for current
    end
  end

  -- go through the line numbers and collapse sequences of numbers
  -- into a line number ranges when possible
  local lineNoStr = ""
  for _i, v in ipairs(list) do
    if lineNoStr == "" then
      lineNoStr = v
    else 
      lineNoStr = lineNoStr .. ',' .. v
    end

    if pending == nil then
      pending = v
      current = v
    else
      if v == current + 1 then
        current = v
      else 
        writePending()
        pending = v
        current = v
      end
    end
  end
  if pending ~= nil then
    writePending()
  end

  return {
    text = val,
    count = valuesWritten,
    lineNumbers = lineNoStr
  }
end

function processLaTeXAnnotation(line, annoteNumber, annotationProvider)
  -- we specially handle LaTeX output in coordination with the post processor
  -- which will replace any of these tokens as appropriate.   
  local hasHighlighting = param('text-highlighting', false)
  if param(kCodeAnnotationsParam) == kCodeAnnotationStyleNone then
    local replaced = annotationProvider.replaceAnnotation(line, annoteNumber, '') 
    return replaced
  else
    if hasHighlighting then
      -- highlighting is enabled, allow the comment through
      local placeholderComment = annotationProvider.createComment("<" .. tostring(annoteNumber) .. ">")
      local replaced = annotationProvider.replaceAnnotation(line, annoteNumber, placeholderComment) 
      return replaced
    else
      -- no highlighting enabled, ensure we use a standard comment character
      local placeholderComment = "%% (" .. tostring(annoteNumber) .. ")"
      local replaced = annotationProvider.replaceAnnotation(line, annoteNumber, placeholderComment) 
      return replaced
    end
  end
end

function processAsciidocAnnotation(line, annoteNumber, annotationProvider)
  if param(kCodeAnnotationsParam) == kCodeAnnotationStyleNone then
    local replaced = annotationProvider.replaceAnnotation(line, annoteNumber, '') 
    return replaced
  else
    local replaced = annotationProvider.replaceAnnotation(line, annoteNumber, " <" .. tostring(annoteNumber) .. ">") 
    return replaced
  end
end

function processAnnotation(line, annoteNumber, annotationProvider)
    -- For all other formats, just strip the annotation- the definition list is converted
    -- to be based upon line numbers. 
        local stripped = annotationProvider.stripAnnotation(line, annoteNumber)
    return stripped
end

function codeMeta()
  return {
    Meta = function(meta)
      if _quarto.format.isLatexOutput() and hasAnnotations then
        -- ensure we have tikx for making the circles
        quarto.doc.use_latex_package("tikz");
        quarto.doc.include_text('in-header', [[
        \newcommand*\circled[1]{\tikz[baseline=(char.base)]{
          \node[shape=circle,draw,inner sep=1pt] (char) {{\scriptsize#1}};}}  
                  ]]);  
      end
    end,

  }
end

-- The actual filter that will look for a code cell and then
-- find its annotations, then process the subsequent OL
function code() 
  -- the localized strings
  local language = param("language", nil);              

  -- walk the blocks and look for annotated code
  -- process the list top down so that we see the outer
  -- code divs first
  return {
    traverse = 'topdown',
    Blocks = function(blocks) 

      -- the user request code annotations value
      local codeAnnotations = param(kCodeAnnotationsParam)

      -- if code annotations is false, then shut it down
      if codeAnnotations ~= false then

        local outputs = pandoc.List()

        -- annotations[annotation-number] = {list of line numbers}
        local pendingAnnotations = nil
        local pendingCellId = nil
        local pendingCodeCell = nil
        local idCounter = 1

        local clearPending = function() 
          pendingAnnotations = nil
          pendingCellId = nil
          pendingCodeCell = nil
        end

        local outputBlockClearPending = function(block)
          if pendingCodeCell then
            outputs:insert(pendingCodeCell)
          end
          outputs:insert(block)
          clearPending()
        end

        local outputBlock = function(block)
          outputs:insert(block)
        end

        local allOutputs = function()
          return outputs
        end

        local resolveCellId = function(identifier) 
          if identifier ~= nil and identifier ~= '' then
            return identifier
          else
            local cellId = 'annotated-cell-' .. tostring(idCounter)
            idCounter = idCounter + 1
            return cellId
          end
        end

        local processCodeCell = function(el, identifier)

          -- select the process for this format's annotations
          local annotationProcessor = processAnnotation
          if _quarto.format.isLatexOutput() then
            annotationProcessor = processLaTeXAnnotation
          elseif _quarto.format.isAsciiDocOutput() then
            annotationProcessor = processAsciidocAnnotation
          end

          -- resolve annotations
          local resolvedCodeBlock, annotations = resolveCellAnnotes(el, annotationProcessor)
          if annotations and next(annotations) ~= nil then
            -- store the annotations and  cell info
            pendingAnnotations = annotations
            pendingCellId = identifier
            
            -- decorate the cell and return it
            if codeAnnotations ~= kCodeAnnotationStyleNone then
              resolvedCodeBlock.attr.classes:insert(kDataCodeAnnonationClz);
            end
            return resolvedCodeBlock
          else
            return nil
          end
        end

        for i, block in ipairs(blocks) do
          if block.t == 'Div' and block.attr.classes:find('cell') then
            -- Process executable code blocks 
            -- In the case of executable code blocks, we actually want
            -- to shift the OL up above the output, so we hang onto this outer
            -- cell so we can move the OL up into it if there are annotations
            local processedAnnotation = false
            local resolvedBlock = _quarto.ast.walk(block, {
              CodeBlock = function(el)
                if el.attr.classes:find('cell-code') then
                  
                  local cellId = resolveCellId(el.attr.identifier)
                  local codeCell = processCodeCell(el, cellId)
                  if codeCell then
                    processedAnnotation = true
                    if codeAnnotations ~= kCodeAnnotationStyleNone then
                      codeCell.attr.identifier = cellId;
                    end
                  end
                  return codeCell
                end
              end
            })
            if processedAnnotation then
              -- we found annotations, so hand onto this cell
              pendingCodeCell = resolvedBlock
            else
              -- no annotations, just output it
              outputBlock(resolvedBlock)
            end
          elseif block.t == 'CodeBlock'  then
            -- don't process code cell output here - we'll get it above
            -- This processes non-executable code blocks
            if not block.attr.classes:find('cell-code') then

              -- If there is a pending code cell and we get here, just
              -- output the pending code cell and continue
              if pendingCodeCell then
                outputBlock(pendingCodeCell)
                clearPending()
              end

              local cellId = resolveCellId(block.attr.identifier)
              local codeCell = processCodeCell(block, cellId)
              if codeCell then
                if codeAnnotations ~= kCodeAnnotationStyleNone then
                  codeCell.attr.identifier = cellId;
                end
                outputBlock(codeCell)
              else
                outputBlockClearPending(block)
              end
            else
              outputBlockClearPending(block)
            end
          elseif block.t == 'OrderedList' and pendingAnnotations ~= nil and next(pendingAnnotations) ~= nil then
            -- There are pending annotations, which means this OL is immediately after
            -- a code cell with annotations. Use to emit a DL describing the code
            local items = pandoc.List()
            for i, v in ipairs(block.content) do
              -- find the annotation for this OL
              local annotationNumber = block.start + i - 1

              local annoteId = toAnnoteId(annotationNumber)
              local annotation = pendingAnnotations[annoteId]
              if annotation then

                local lineNumMeta = lineNumberMeta(annotation)

                -- compute the term for the DT
                local term = ""
                if _quarto.format.isLatexOutput() then
                  term = latexListPlaceholder(annotationNumber)
                elseif _quarto.format.isAsciiDocOutput() then
                  term = "<" .. tostring(annotationNumber) .. ">"
                else
                  if lineNumMeta.count == 1 then
                    term = language[kCodeLine] .. " " .. lineNumMeta.text;
                  else
                    term = language[kCodeLines] .. " " .. lineNumMeta.text;
                  end
                end

                -- compute the definition for the DD
                local definitionContent = v[1].content 
                local annotationToken = tostring(annotationNumber);

                -- Only output span for certain formats (HTML)
                -- for markdown / gfm we should drop the spans
                local definition = nil
                if _quarto.format.isHtmlOutput() then
                  definition = pandoc.Span(definitionContent, {
                    [kDataCodeCellTarget] = pendingCellId,
                    [kDataCodeCellLines] = lineNumMeta.lineNumbers,
                    [kDataCodeCellAnnotation] = annotationToken
                  });
                else 
                  definition = pandoc.Plain(definitionContent)
                end

                -- find the lines that annotate this and convert to a DL
                items:insert({
                  term,
                  definition})
              else
                -- there was an OL item without a corresponding annotation
                warn("List item " .. tostring(i) .. " has no corresponding annotation in the code cell\n(" .. pandoc.utils.stringify(v) ..  ")")
              end
            end

            -- add the definition list
            local dl
            if _quarto.format.isAsciiDocOutput() then
              local formatted = pandoc.List()
              for _i,v in ipairs(items) do
                local annotationMarker = v[1] .. ' '
                local definition = v[2]
                tprepend(definition.content, {annotationMarker})
                formatted:insert(definition)
              end
              dl = pandoc.Div(formatted)
            else
              dl = pandoc.DefinitionList(items)
            end

            -- if there is a pending code cell, then insert into that and add it
            if codeAnnotations ~= kCodeAnnotationStyleNone then
              if pendingCodeCell ~= nil then
                -- wrap the definition list in a cell
                local dlDiv = pandoc.Div({dl}, pandoc.Attr("", {kCellAnnotationClass}))
                pendingCodeCell.content:insert(2, dlDiv)
                outputBlock(pendingCodeCell)
                clearPending();
              else
                outputBlockClearPending(dl)
              end
            else
              if pendingCodeCell then
                outputBlock(pendingCodeCell)
              end
              clearPending();
            end
          else
            outputBlockClearPending(block)
          end
        end
        return allOutputs()
      end
    end
  }
end
-- for code blocks w/ filename create an enclosing div:
-- <div class="code-with-filename">
--   <div class="code-with-filename-file">
--     <pre>filename.py</pre>
--   </div>
--   <div class="sourceCode" id="cb1" data-filename="filename.py">
--     <pre></pre>
--   </div>
-- </div>

local function codeBlockWithFilename(el, filename)
  return pandoc.Plain(quarto.DecoratedCodeBlock({
    filename = filename,
    code_block = el:clone()
  }))
  -- if _quarto.format.isHtmlOutput() then
  --   local filenameEl = pandoc.Div({pandoc.Plain{
  --     pandoc.RawInline("html", "<pre>"),
  --     pandoc.Strong{pandoc.Str(filename)},
  --     pandoc.RawInline("html", "</pre>")
  --   }}, pandoc.Attr("", {"code-with-filename-file"}))
  --   return pandoc.Div(
  --     { filenameEl, el:clone() },
  --     pandoc.Attr("", {"code-with-filename"})
  --   )
  -- else
  --   return pandoc.Div(
  --     { pandoc.Plain{pandoc.Strong{pandoc.Str(filename)}}, el:clone() },
  --     pandoc.Attr("", {"code-with-filename"})
  --   )
  -- end
end

function codeFilename() 
  return {
    Blocks = function(blocks)
  
      -- transform ast for 'filename'
      local foundFilename = false
      local newBlocks = pandoc.List()
      for _,block in ipairs(blocks) do
        if block.attributes ~= nil and block.attributes["filename"] then
          local filename = block.attributes["filename"]
          if block.t == "CodeBlock" then
            foundFilename = true
            block.attributes["filename"] = nil
            newBlocks:insert(codeBlockWithFilename(block, filename))
          elseif block.t == "Div" and block.content[1].t == "CodeBlock" then
            foundFilename = true
            block.attributes["filename"] = nil
            block.content[1] = codeBlockWithFilename(block.content[1], filename)
            newBlocks:insert(block)
          else
            newBlocks:insert(block)
          end
        else
          newBlocks:insert(block)
        end
      end
    
      -- if we found a file name then return the modified list of blocks
      if foundFilename then
        return newBlocks
      else
        return blocks
      end
    end
  }  
end
-- content-hidden.lua
-- Copyright (C) 2022 Posit Software, PBC


local kContentVisible = "content-visible"
local kContentHidden = "content-hidden"
local kWhenFormat = "when-format"
local kUnlessFormat = "unless-format"
local kWhenProfile = "when-profile"
local kUnlessProfile = "unless-profile"
local kConditions = pandoc.List({kWhenFormat, kUnlessFormat, kWhenProfile, kUnlessProfile})

function is_visible(node)
  local profiles = pandoc.List(param("quarto_profile", {}))
  local match = propertiesMatch(node.condition, profiles)
  if node.behavior == kContentVisible then
    return match
  elseif node.behavior == kContentHidden then
    return not match
  else
    crash_with_stack_trace()
    return false
  end
end

_quarto.ast.add_handler({
  class_name = { kContentVisible, kContentHidden },
  
  ast_name = "ConditionalBlock",

  kind = "Block",

  parse = function(div)
    local behavior = div.classes:find(kContentVisible) or div.classes:find(kContentHidden)
    local condition = pandoc.List({})
    local remaining_attributes = pandoc.List({})
    for i, v in ipairs(div.attributes) do
      if kConditions:find(v[1]) ~= nil then
        condition:insert(v)
      else
        remaining_attributes:insert(v)
      end
    end
    div.attributes = remaining_attributes
    div.classes = div.classes:filter(function(k) return k ~= kContentVisible and k ~= kContentHidden end)
    return quarto.ConditionalBlock({
      node = div,
      behavior = behavior,
      condition = condition
    })
  end,

  render = function(node)
    local el = node.node
    local visible = is_visible(node)
    clearHiddenVisibleAttributes(el)
    if visible then
      return el.content
    else
      return {}
    end
  end,

  constructor = function(tbl)
    local result = {
      node = tbl.node,
      behavior = tbl.behavior,
      condition = pandoc.List({})
    };
    for i, v in ipairs(tbl.condition or {}) do
      if kConditions:find(v[1]) == nil then
        error("Ignoring invalid condition in conditional block: " .. v[1])
      else
        result.condition[v[1]] = v[2]
      end
    end

    return result
  end,

  inner_content = function(tbl)
    if is_visible(tbl) then
      return {
        content = tbl.node.content,
      }
    else
      return {}
    end
  end,

  set_inner_content = function(tbl, content)
    if content.content ~= nil then
      tbl.node.content = content.content
    end
  end
})

function contentHidden()
  local profiles = pandoc.List(param("quarto_profile", {}))
  return {
    -- Div = handleHiddenVisible(profiles),
    CodeBlock = handleHiddenVisible(profiles),
    Span = handleHiddenVisible(profiles)
  }
end

function handleHiddenVisible(profiles)
  return function(el)
    local visible
    if el.attr.classes:find(kContentVisible) then
      clearHiddenVisibleAttributes(el)
      visible = propertiesMatch(el.attributes, profiles)
    elseif el.attr.classes:find(kContentHidden) then
      clearHiddenVisibleAttributes(el)
      visible = not propertiesMatch(el.attributes, profiles)
    else
      return el
    end
    -- this is only called on spans and codeblocks, so here we keep the scaffolding element
    -- as opposed to in the Div where we return the inlined content
    if visible then
      return el
    else
      return {}
    end
  end
end

-- "properties" here will come either from "conditions", in the case of a custom AST node
-- or from the attributes of the element itself in the case of spans or codeblocks
function propertiesMatch(properties, profiles)
  local match = true
  if properties[kWhenFormat] ~= nil then
    match = match and _quarto.format.isFormat(properties[kWhenFormat])
  end
  if properties[kUnlessFormat] ~= nil then
    match = match and not _quarto.format.isFormat(properties[kUnlessFormat])
  end
  if properties[kWhenProfile] ~= nil then
    match = match and profiles:includes(properties[kWhenProfile])
  end
  if properties[kUnlessProfile] ~= nil then
    match = match and not profiles:includes(properties[kUnlessProfile])
  end
  return match
end

function clearHiddenVisibleAttributes(el)
  el.attributes[kUnlessFormat] = nil
  el.attributes[kWhenFormat] = nil
  el.attributes[kUnlessProfile] = nil
  el.attributes[kWhenProfile] = nil
  el.attr.classes = removeClass(el.attr.classes, kContentVisible)
  el.attr.classes = removeClass(el.attr.classes, kContentHidden)
end
-- engine-escape.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

local kEngineEscapePattern = "{({+([^}]+)}+)}"

function engineEscape()
  return {
    CodeBlock = function(el)

      -- handle code block with 'escaped' language engine
      if #el.attr.classes == 1 then
        local engine, lang = el.attr.classes[1]:match(kEngineEscapePattern)
        if engine then
          el.text = "```" .. engine .. "\n" .. el.text .. "\n" .. "```"
          el.attr.classes[1] = "markdown"
          return el
        end
      end

      -- handle escaped engines within a code block
      el.text = el.text:gsub("```" .. kEngineEscapePattern, function(engine, lang)
        if #el.attr.classes == 0 or not isHighlightClass(el.attr.classes[1]) then
          el.attr.classes:insert(1, "markdown")
        end
        return "```" .. engine 
      end)
      return el
    end
  }
end


local kHighlightClasses = {
  "abc",
  "actionscript",
  "ada",
  "agda",
  "apache",
  "asn1",
  "asp",
  "ats",
  "awk",
  "bash",
  "bibtex",
  "boo",
  "c",
  "changelog",
  "clojure",
  "cmake",
  "coffee",
  "coldfusion",
  "comments",
  "commonlisp",
  "cpp",
  "cs",
  "css",
  "curry",
  "d",
  "default",
  "diff",
  "djangotemplate",
  "dockerfile",
  "dot",
  "doxygen",
  "doxygenlua",
  "dtd",
  "eiffel",
  "elixir",
  "elm",
  "email",
  "erlang",
  "fasm",
  "fortranfixed",
  "fortranfree",
  "fsharp",
  "gcc",
  "glsl",
  "gnuassembler",
  "go",
  "graphql",
  "groovy",
  "hamlet",
  "haskell",
  "haxe",
  "html",
  "idris",
  "ini",
  "isocpp",
  "j",
  "java",
  "javadoc",
  "javascript",
  "javascriptreact",
  "json",
  "jsp",
  "julia",
  "kotlin",
  "latex",
  "lex",
  "lilypond",
  "literatecurry",
  "literatehaskell",
  "llvm",
  "lua",
  "m4",
  "makefile",
  "mandoc",
  "markdown",
  "mathematica",
  "matlab",
  "maxima",
  "mediawiki",
  "metafont",
  "mips",
  "modelines",
  "modula2",
  "modula3",
  "monobasic",
  "mustache",
  "nasm",
  "nim",
  "noweb",
  "objectivec",
  "objectivecpp",
  "ocaml",
  "octave",
  "opencl",
  "pascal",
  "perl",
  "php",
  "pike",
  "postscript",
  "povray",
  "powershell",
  "prolog",
  "protobuf",
  "pure",
  "purebasic",
  "python",
  "qml",
  "r",
  "raku",
  "relaxng",
  "relaxngcompact",
  "rest",
  "rhtml",
  "roff",
  "ruby",
  "rust",
  "scala",
  "scheme",
  "sci",
  "sed",
  "sgml",
  "sml",
  "spdxcomments",
  "sql",
  "sqlmysql",
  "sqlpostgresql",
  "stata",
  "swift",
  "tcl",
  "tcsh",
  "texinfo",
  "toml",
  "typescript",
  "verilog",
  "vhdl",
  "xml",
  "xorg",
  "xslt",
  "xul",
  "yacc",
  "yaml",
  "zsh"
}

function isHighlightClass(class)
  for _, v in ipairs (kHighlightClasses) do
    if v == class then
      return true
    end
  end
  return false
end
-- figures.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function quartoPreFigures() 
  
  return {
   
    Div = function(el)
      
      -- propagate fig-cap on figure div to figure caption 
      if hasFigureRef(el) then
        local figCap = attribute(el, kFigCap, nil)
        if figCap ~= nil then
          local caption = pandoc.Para(markdownToInlines(figCap))
          el.content:insert(caption)
          el.attr.attributes[kFigCap] = nil
        end
      end
      return el
      
    end,
    
    -- create figure divs from linked figures
    Para = function(el)
      
      -- create figure div if there is a tikz image
      local fig = discoverFigure(el)
      if fig and latexIsTikzImage(fig) then
        return createFigureDiv(el, fig)
      end
      
      -- create figure divs from linked figures
      local linkedFig = discoverLinkedFigure(el)
      if linkedFig then
        return createFigureDiv(el, linkedFig)
      end

    end,

    Image = function(image)
      -- propagate fig-alt
      if _quarto.format.isHtmlOutput() then
        -- read the fig-alt text and set the image alt
        local altText = attribute(image, kFigAlt, nil);
        if altText ~= nil then
          image.attr.attributes["alt"] = altText
          image.attr.attributes[kFigAlt] = nil
          return image
        end
      -- provide default fig-pos or fig-env if specified
      elseif _quarto.format.isLatexOutput() then
        local figPos = param(kFigPos)
        if figPos and not image.attr.attributes[kFigPos] then
          image.attr.attributes[kFigPos] = figPos
        end
        -- remove fig-pos if it is false, since it
        -- signals "don't use any value"
        if image.attr.attributes[kFigPos] == "FALSE" then
          image.attr.attributes[kFigPos] = nil
        end
        local figEnv = param(kFigEnv)
        
        if figEnv and not image.attr.attributes[kFigEnv] then
          image.attr.attributes[kFigEnv] = figEnv
        end
      else 
        return image
      end
    end
  }
end



-- hidden.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function hidden()
  if (param("keep-hidden", false)) then
    return {
      Div = stripHidden,
      CodeBlock = stripHidden
    }
  else
    return {

    }
  end
end

function stripHidden(el)
  if not _quarto.format.isHtmlOutput() and el.attr.classes:find("hidden") then
    return {}
  end
end
-- include-paths.lua
--
-- fixes paths from <include> directives
--
-- Copyright (C) 2022 Posit Software, PBC

function includePaths() 
  return {
    Link = function(el)
      local file = currentFileMetadataState().file
      if file ~= nil and file.include_directory ~= nil then
        el.target = fixIncludePath(el.target, file.include_directory)
      end
      return el
    end,

    Image = function(el)
      local file = currentFileMetadataState().file
      if file ~= nil and file.include_directory ~= nil then 
        el.src = fixIncludePath(el.src, file.include_directory)
      end
      return el
    end,

    RawInline = handleRawElementIncludePath,
    RawBlock = handleRawElementIncludePath,
  }
end


function handleRawElementIncludePath(el)
  if _quarto.format.isRawHtml(el) then
    local file = currentFileMetadataState().file
    if file ~= nil and file.include_directory ~= nil then
      handlePaths(el, file.include_directory, fixIncludePath)
    end
    return el
  end
end
-- input-traits.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function addInputTrait(key, value)
  preState.results.inputTraits[key] = value
end

local kPositionedRefs = 'positioned-refs'
function inputTraits() 
  return {
    Div = function(el) 
      local hasPositionedRefs = el.attr.identifier == 'refs'
      if (hasPositionedRefs) then
        addInputTrait(kPositionedRefs, true) 
      end
    end
  }
end
-- line-numbers.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


function lineNumbers()
  return {
    CodeBlock = function(el)
      if #el.attr.classes > 0 then
        local lineNumbers = lineNumbersAttribute(el)
        if lineNumbers ~= false then
          -- use the pandoc line numbering class
          el.attr.classes:insert("number-lines")
          -- remove for all formats except reveal
          if not _quarto.format.isRevealJsOutput() then
            el.attr.attributes["code-line-numbers"] = nil
          end
          return el
        end
      end
    end
  }
end

function lineNumbersAttribute(el)
  local default = param("code-line-numbers", false)
  local lineNumbers = attribute(el, "code-line-numbers", default)
  if lineNumbers == true or lineNumbers == "true" or lineNumbers == "1" then
    return true
  elseif lineNumbers == false or lineNumbers == "false" or lineNumbers == "0" then
    return false
  else
    return tostring(lineNumbers)
  end
end
-- meta.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- inject metadata
function quartoPreMetaInject()
  return {
    Meta = function(meta)

      -- injection awesomebox for captions, if needed
      if preState.hasCallouts and _quarto.format.isLatexOutput() then
        metaInjectLatex(meta, function(inject)
          inject(
            usePackageWithOption("tcolorbox", "skins,breakable")
          )
          inject(
            usePackage("fontawesome5")
          )
          inject(
            "\\definecolor{quarto-callout-color}{HTML}{" .. kColorUnknown .. "}\n" ..
            "\\definecolor{quarto-callout-note-color}{HTML}{" .. kColorNote .. "}\n" ..
            "\\definecolor{quarto-callout-important-color}{HTML}{" .. kColorImportant .. "}\n" ..
            "\\definecolor{quarto-callout-warning-color}{HTML}{" .. kColorWarning .."}\n" ..
            "\\definecolor{quarto-callout-tip-color}{HTML}{" .. kColorTip .."}\n" ..
            "\\definecolor{quarto-callout-caution-color}{HTML}{" .. kColorCaution .. "}\n" ..
            "\\definecolor{quarto-callout-color-frame}{HTML}{" .. kColorUnknownFrame .. "}\n" ..
            "\\definecolor{quarto-callout-note-color-frame}{HTML}{" .. kColorNoteFrame .. "}\n" ..
            "\\definecolor{quarto-callout-important-color-frame}{HTML}{" .. kColorImportantFrame .. "}\n" ..
            "\\definecolor{quarto-callout-warning-color-frame}{HTML}{" .. kColorWarningFrame .."}\n" ..
            "\\definecolor{quarto-callout-tip-color-frame}{HTML}{" .. kColorTipFrame .."}\n" ..
            "\\definecolor{quarto-callout-caution-color-frame}{HTML}{" .. kColorCautionFrame .. "}\n"
          )
        end)
      end

      metaInjectLatex(meta, function(inject)
        if preState.usingTikz then
          inject(usePackage("tikz"))
        end
      end)


      metaInjectLatex(meta, function(inject)
        if preState.usingBookmark then
          inject(
            usePackage("bookmark")
          )    
        end
      end)

      return meta
    end
  }
end

-- options.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


local allOptions = {}

-- initialize options from 'crossref' metadata value
function initOptions()
  return {
    Meta = function(meta)
      if meta ~= nil then
        allOptions = readMetaOptions(meta)
      end
    end
  }
end

-- get option value
function option(name, def)
  return parseOption(name, allOptions, def)
end

local kVarNamespace = "_quarto-vars"
function var(name, def)
  local vars = allOptions[kVarNamespace]
  if vars ~= nil then
    return parseOption(name, vars, def)
  else
    return nil
  end
end

function parseOption(name, options, def) 
  local keys = split(name, ".")

  local value = nil
  for i, key in ipairs(keys) do
    if value == nil then
      value = readOption(options, key, nil)
    else
      value = value[key]

      -- the key doesn't match a value, stop indexing
      if value == nil then
        break
      end
    end
  end
  if value == nil then
    return def
  else
    return value
  end
end

function capLocation(scope, default)
  local loc = option(scope .. '-cap-location', option('cap-location', nil))
  if loc ~= nil then
    return inlinesToString(loc)
  else
    return default
  end
end
-- output-location.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

local function collectCellOutputLocation(el)
  if el.t == "Div" and 
     el.attr.classes:includes("cell")  then
    local outputLoc = el.attr.attributes["output-location"]
    el.attr.attributes["output-location"] = nil 
    if outputLoc == nil then
      outputLoc = param('output-location')
    end
    return outputLoc
  else
    return nil
  end
        
end

local function outputLocationCellHasCode(el)
  return #el.content > 0 and
         el.content[1].t == "CodeBlock" and
         el.content[1].attr.classes:includes("cell-code")  
end

-- note: assumes that outputLocationCellHasCode has been called
local function partitionCell(el, outputClass)
  -- compute the code div, being sure to bring the annotations 
  -- along with the code
  local code = { el.content[1] }
  local outputIndex
  if isAnnotationCell(el.content[2]) then
    tappend(code, {el.content[2]})
    outputIndex = 3
  else
    outputIndex = 2
  end

  local codeDiv = pandoc.Div(code, el.attr)

  local outputDiv = pandoc.Div(tslice(el.content, outputIndex, #el.content), el.attr)
  outputDiv.attr.identifier = ""
  outputDiv.attr.classes:insert(outputClass)
  return { codeDiv, outputDiv }
end

local function fragmentOutputLocation(block)
  return partitionCell(block, "fragment")
end

local function slideOutputLocation(block)
  return partitionCell(block, "output-location-slide")
end

local function columnOutputLocation(el, fragment)
  local codeDiv = pandoc.Div({ el.content[1] })
  local outputDiv = pandoc.Div(tslice(el.content, 2, #el.content))
  codeDiv.attr.classes:insert("column")
  outputDiv.attr.identifier = ""
  outputDiv.attr.classes:insert("column")
  if fragment then
    outputDiv.attr.classes:insert("fragment")
  end
  local columnsDiv = pandoc.Div( {codeDiv, outputDiv}, el.attr )
  tappend(columnsDiv.attr.classes, {
    "columns", "column-output-location"
  })
  return { columnsDiv }
end

function outputLocation()
  if _quarto.format.isRevealJsOutput() then
    return {
      Blocks = function(blocks)
        local newBlocks = pandoc.List()
        for _,block in pairs(blocks) do
          local outputLoc = collectCellOutputLocation(block)
          if outputLoc then
            if outputLocationCellHasCode(block) then
              if outputLoc == "fragment" then
                tappend(newBlocks, fragmentOutputLocation(block))
              elseif outputLoc == "column" then
                tappend(newBlocks, columnOutputLocation(block))
              elseif outputLoc == "column-fragment" then
                tappend(newBlocks, columnOutputLocation(block, true))
              elseif outputLoc == "slide" then
                tappend(newBlocks, slideOutputLocation(block))
              else
                newBlocks:insert(block)
              end
            else
              warn("output-location is only valid for cells that echo their code")
              newBlocks:insert(block)
            end
          else
            newBlocks:insert(block)
          end
        end
        return newBlocks
      end
    }
  else
    return {}
  end
 
end






function outputs()
  return {
    -- unroll output divs for formats (like pptx) that don't support them
    Div = function(div)

      -- if we don't support output divs then we need to unroll them
      if not param("output-divs", true) then
        if tcontains(div.attr.classes, "cell") then
          -- if this is PowerPoint and it's a figure panel then let it through (as
          -- we'll use PowerPoint columns to layout at least 2 figures side-by-side)
          if _quarto.format.isPowerPointOutput() and hasLayoutAttributes(div) then
            return nil
          end
  
          -- unroll blocks contained in divs
          local blocks = pandoc.List()
          for _, childBlock in ipairs(div.content) do
            if childBlock.t == "Div" then
              tappend(blocks, childBlock.content)
            else
              blocks:insert(childBlock)
            end
          end
      
          return blocks
        end
      end
    end
  }
end
-- panel-input.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function panelInput() 

  return {
    Div = function(el)
      if hasBootstrap() and el.attr.classes:find("panel-input") then
        tappend(el.attr.classes, {
          "card",
          "bg-light",
          "p-2",
        })
      end
      return el
    end
  }


end

-- panel-layout.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function panelLayout() 

  return {
    Div = function(el)
      if (hasBootstrap() and el.t == "Div") then
        local fill = el.attr.classes:find("panel-fill")
        local center = el.attr.classes:find("panel-center")
        if fill or center then
          local layoutClass =  fill and "panel-fill" or "panel-center"
          local div = pandoc.Div({ el })
          el.attr.classes = el.attr.classes:filter(function(clz) return clz ~= layoutClass end)
          if fill then
            tappend(div.attr.classes, {
              "g-col-24",
            })
          elseif center then
            tappend(div.attr.classes, {
              "g-col-24",
              "g-col-lg-20",
              "g-start-lg-2"
            })
          end
          -- return wrapped in a raw
          return pandoc.Div({ div }, pandoc.Attr("", { 
            layoutClass,
            "panel-grid"
          }))
        end
      end
      return el
    end
  }
  
end

-- panel-sidebar.lua
-- Copyright (C) 2021-2022 Posit Software, PBC

function panelSidebar() 
  return {
    Blocks = function(blocks)
      if hasBootstrap() or _quarto.format.isRevealJsOutput() then

        -- functions to determine if an element has a layout class
        local function isSidebar(el)
          return el ~= nil and el.t == "Div" and el.attr.classes:includes("panel-sidebar")
        end
        local function isContainer(el)
          return el ~= nil and
                 el.t == "Div" and 
                 (el.attr.classes:includes("panel-fill") or 
                  el.attr.classes:includes("panel-center") or
                  el.attr.classes:includes("panel-tabset"))
        end
        local function isHeader(el)
          return el ~= nil and el.t == "Header"
        end
        local function isQuartoHiddenDiv(el)
          return el ~= nil and el.t == "Div" and
                 string.find(el.attr.identifier, "^quarto%-") and
                 el.attr.classes:includes("hidden")
        end
        local function isNotQuartoHiddenDiv(el)
          return not isQuartoHiddenDiv(el)
        end

        -- bail if there are no sidebars
        local sidebar, sidebarIdx = blocks:find_if(isSidebar)
        if not sidebar then
          return blocks
        end

        -- create sidebar handler and get attr
        local sidebarHandler = bootstrapSidebar()
        if _quarto.format.isRevealJsOutput() then
          sidebarHandler = revealSidebar()
        end
        local sidebarAttr = sidebarHandler.sidebarAttr()
        local containerAttr = sidebarHandler.containerAttr()
    
        -- filter out quarto hidden blocks (they'll get put back in after processing)
        local quartoHiddenDivs = blocks:filter(isQuartoHiddenDiv)
        blocks = blocks:filter(isNotQuartoHiddenDiv)

        -- locate and arrange sidebars until there are none left
        local sidebar, sidebarIdx = blocks:find_if(isSidebar)
       
        while sidebar ~= nil and sidebarIdx ~= nil do

          -- always transfer sidebar attributes to sidebar
          transferAttr(sidebarAttr, sidebar.attr)

          -- sidebar after container
          if isContainer(blocks[sidebarIdx - 1]) then
            blocks:remove(sidebarIdx)
            local container = blocks:remove(sidebarIdx - 1)
            transferAttr(containerAttr, container.attr)
            blocks:insert(sidebarIdx - 1, 
              pandoc.Div({ container, sidebar }, sidebarHandler.rowAttr({"layout-sidebar-right"}))
            )
          -- sidebar before container
          elseif isContainer(blocks[sidebarIdx + 1]) then
            local container = blocks:remove(sidebarIdx + 1)
            transferAttr(containerAttr, container.attr)
            blocks:remove(sidebarIdx)
            blocks:insert(sidebarIdx, 
              pandoc.Div({ sidebar, container }, sidebarHandler.rowAttr({"layout-sidebar-left"}))
            )
          else
            -- look forward for a header
            local header, headerIdx = blocks:find_if(isHeader, sidebarIdx)
            if header and headerIdx and (headerIdx ~= (sidebarIdx + 1)) then
              local panelBlocks = pandoc.List()
              for i = sidebarIdx + 1, headerIdx - 1, 1 do
                panelBlocks:insert(blocks:remove(sidebarIdx + 1))
              end
              local panelFill = pandoc.Div(panelBlocks, pandoc.Attr("", { "panel-fill" }))
              transferAttr(containerAttr, panelFill)
              blocks:remove(sidebarIdx)
              blocks:insert(sidebarIdx, 
                pandoc.Div({ sidebar,  panelFill }, sidebarHandler.rowAttr({"layout-sidebar-left"}))
              )
            else
              -- look backwards for a header 
              
              headerIdx = nil
              for i = sidebarIdx - 1, 1, -1 do
                if isHeader(blocks[i]) then
                  headerIdx = i
                  break
                end
              end
              -- if we have a header then collect up to it
              if headerIdx ~= nil and (headerIdx ~= (sidebarIdx - 1)) then
                local panelBlocks = pandoc.List()
                for i = headerIdx + 1, sidebarIdx - 1, 1 do
                  panelBlocks:insert(blocks:remove(headerIdx + 1))
                end
                local panelFill = pandoc.Div(panelBlocks,  pandoc.Attr("", { "panel-fill" }))
                transferAttr(containerAttr, panelFill)
                blocks:remove(headerIdx + 1)
                blocks:insert(headerIdx + 1, 
                  pandoc.Div({ panelFill, sidebar }, sidebarHandler.rowAttr({"layout-sidebar-right"}))
                )
              else
                --  no implicit header containment found, strip the sidebar attribute
                sidebar.attr.classes = sidebar.attr.classes:filter(
                  function(clz) 
                    return clz ~= "panel-sidebar" and clz ~= "panel-input"
                  end
                )
              end
            end
          end

          -- try to find another sidebar
          sidebar, sidebarIdx = blocks:find_if(isSidebar)
        end

        -- restore hidden divs and return blocks
        tappend(blocks, quartoHiddenDivs)
        return blocks
      end
    end
  }
end

function bootstrapSidebar()
  return {
    rowAttr = function(classes)
      local attr = pandoc.Attr("", {
        "panel-grid", 
        "layout-sidebar",
        "ms-md-0"
      })
      tappend(attr.classes, classes)
      return attr
    end,
    sidebarAttr = function()
      return pandoc.Attr("", {
        "card",
        "bg-light",
        "p-2",
        "g-col-24",
        "g-col-lg-7"
      })
    end,
    containerAttr = function()
      return pandoc.Attr("", {
        "g-col-24",
        "g-col-lg-17",
        "pt-3",
        "pt-lg-0",
      })
    end
  }
end

function revealSidebar()
  return {
    rowAttr = function(classes) 
      local attr = pandoc.Attr("", { "layout-sidebar" })
      tappend(attr.classes, classes)
      return attr
    end,
    sidebarAttr = function()
      local attr = pandoc.Attr("", {})
      return attr
    end,
    containerAttr = function()
      return pandoc.Attr("")
    end
  }
end

function transferAttr(from, to)
  tappend(to.classes, from.classes)
  for k,v in pairs(from.attributes) do
    to.attributes[k] = v
  end
end
-- panel-tabset.lua
-- Copyright (C) 2022 Posit Software, PBC

---@alias quarto.Tab { content:pandoc.Blocks, title:pandoc.Inlines }

--[[
Create a Tab AST node (represented as a Lua table)
]]
---@param params { content:nil|pandoc.Blocks|string, title:pandoc.Inlines|string }
---@return quarto.Tab
quarto.Tab = function(params)
  local content
  if type(params.content) == "string" then
    local content_string = params.content
    ---@cast content_string string
    content = pandoc.Blocks(pandoc.read(content_string, "markdown").blocks)
  else
    content = params.content or pandoc.Blocks({})
  end
  return {
    content = content,
    title = pandoc.Inlines(params.title)
  }
end

local function render_quarto_tab(tbl, tabset)
  local content = tbl.content
  local title = tbl.title
  local inner_content = pandoc.List()
  inner_content:insert(pandoc.Header(tabset.level, title))
  inner_content:extend(content)
  return pandoc.Div(inner_content)
end

function parse_tabset_contents(div)
  local heading = div.content:find_if(function(el) return el.t == "Header" end)
  if heading ~= nil then
    -- note the level, then build tab buckets for content after these levels
    local level = heading.level
    local tabs = pandoc.List()
    local tab = nil
    for i=1,#div.content do 
      local el = div.content[i]
      if el.t == "Header" and el.level == level then
        tab = quarto.Tab({ title = el.content })
        tabs:insert(tab)
      elseif tab ~= nil then
        tab.content:insert(el)
      end
    end
    return tabs, level
  else
    return nil
  end
end

local tabsetidx = 1

function render_tabset(attr, tabs, renderer)
  -- create a unique id for the tabset
  local tabsetid = "tabset-" .. tabsetidx
  tabsetidx = tabsetidx + 1

  -- init tab navigation 
  local nav = pandoc.List()
  nav:insert(pandoc.RawInline('html', '<ul ' .. renderer.ulAttribs(tabsetid) .. '>'))

  -- init tab panes
  local panes = pandoc.Div({}, attr)
  panes.attr.classes = attr.classes:map(function(class) 
    if class == "panel-tabset" then
      return "tab-content" 
    else
      return class
    end
  end)
  
  -- populate
  for i=1,#tabs do
    -- alias tab and heading
    local tab = tabs[i]
    local heading = tab.content[1]
    tab.content:remove(1)

    -- tab id
    local tabid = tabsetid .. "-" .. i
    local tablinkid = tabid .. "-tab" -- FIXME unused from before?

    -- navigation
    nav:insert(pandoc.RawInline('html', '<li ' .. renderer.liAttribs() .. '>'))
    nav:insert(pandoc.RawInline('html', '<a ' .. renderer.liLinkAttribs(tabid, i==1) .. '>'))
    nav:extend(heading.content)
    nav:insert(pandoc.RawInline('html', '</a></li>'))

    -- pane
    local paneAttr = renderer.paneAttribs(tabid, i==1, heading.attr)
    local pane = pandoc.Div({}, paneAttr)
    pane.content:extend(tab.content)
    panes.content:insert(pane)
  end

  -- end tab navigation
  nav:insert(pandoc.RawInline('html', '</ul>'))

  -- return tabset
  return pandoc.Div({
    pandoc.Plain(nav),
    panes
  }, attr:clone())
end

_quarto.ast.add_handler({
  -- use either string or array of strings
  class_name = { "panel-tabset" },

  -- the name of the ast node, used as a key in extended ast filter tables
  ast_name = "Tabset",

  kind = "Block",

  constructor = function(params)
    return {
      level = params.level or 2,
      tabs = params.tabs or pandoc.List(),
      attr = params.attr or pandoc.Attr(),
    }
  end,

  -- a function that takes the div node as supplied in user markdown
  -- and returns the custom node
  parse = function(div)
    local tabs, level = parse_tabset_contents(div)
    return quarto.Tabset({
      level = level,
      tabs = tabs,
      attr = div.attr
    })
  end,

  -- a function that renders the extendedNode into output
  render = function(node)
    local tabs = tmap(node.tabs, function(tab) return render_quarto_tab(tab, node) end)
    if hasBootstrap() then
      return render_tabset(node.attr, tabs, bootstrapTabs())
    elseif _quarto.format.isHtmlOutput() then
      return render_tabset(node.attr, tabs, tabbyTabs())
    elseif _quarto.format.isLatexOutput() or _quarto.format.isDocxOutput() or _quarto.format.isEpubOutput() or _quarto.format.isJatsOutput() then
      return pandoc.Div(render_tabset_with_l4_headings(tabs), node.attr)
    else
      print("Warning: couldn't recognize format, using default tabset rendering")
      return pandoc.Div(render_tabset_with_l4_headings(tabs), node.attr)
    end  
  end,

  -- a function that takes the extended node and
  -- returns a table with walkable attributes (pandoc nodes, Inlines, Blocks)
  -- that represent inner content that should
  -- be visible to filters.
  inner_content = function(extended_node)
    local result = {}

    for i=1,#extended_node.tabs do
      result[i * 2 - 1] = extended_node.tabs[i].content
      result[i * 2] = extended_node.tabs[i].title
    end
    return result
  end,

  -- a function that updates the extended node
  -- with new inner content (as returned by filters)
  -- table keys are a subset of those returned by inner_content
  -- and represent changed values that need to be updated.    
  set_inner_content = function(extended_node, values)
    for k, v in pairs(values) do
      local tab = ((k - 1) // 2) + 1
      local key = ((k % 2 == 0) and "title") or "content"
      extended_node.tabs[tab][key] = v
    end
  end
})

-- function tabsetDiv(div, renderer)

--   -- create a unique id for the tabset
--   local tabsetid = "tabset-" .. tabsetidx
--   tabsetidx = tabsetidx + 1

--   -- find the first heading in the tabset
--   local heading = div.content:find_if(function(el) return el.t == "Header" end)
--   if heading ~= nil then
--     -- note the level, then build tab buckets for content after these levels
--     local level = heading.level
--     local tabs = pandoc.List()
--     local tab = nil
--     for i=1,#div.content do 
--       local el = div.content[i]
--       if el.t == "Header" and el.level == level then
--         tab = pandoc.Div({})
--         tab.content:insert(el)
--         tabs:insert(tab)
--       elseif tab ~= nil then
--         tab.content:insert(el)
--       end
--     end

--     -- init tab navigation 
--     local nav = pandoc.List()
--     nav:insert(pandoc.RawInline('html', '<ul ' .. renderer.ulAttribs(tabsetid) .. '>'))

--     -- init tab panes
--     local panes = pandoc.Div({}, div.attr)
--     panes.attr.classes = div.attr.classes:map(function(class) 
--       if class == "panel-tabset" then
--         return "tab-content" 
--       else
--         return class
--       end
--     end)
   
--     -- populate
--     for i=1,#tabs do
--       -- alias tab and heading
--       local tab = tabs[i]
--       local heading = tab.content[1]
--       tab.content:remove(1)

--       -- tab id
--       local tabid = tabsetid .. "-" .. i
--       local tablinkid = tabid .. "-tab"

--       -- navigation
--       nav:insert(pandoc.RawInline('html', '<li ' .. renderer.liAttribs() .. '>'))
--       nav:insert(pandoc.RawInline('html', '<a ' .. renderer.liLinkAttribs(tabid, i==1) .. '>'))
--       nav:extend(heading.content)
--       nav:insert(pandoc.RawInline('html', '</a></li>'))

--       -- pane
--       local paneAttr = renderer.paneAttribs(tabid, i==1, heading.attr)
--       local pane = pandoc.Div({}, paneAttr)
--       pane.content:extend(tab.content)
--       panes.content:insert(pane)
--     end

--     -- end tab navigation
--     nav:insert(pandoc.RawInline('html', '</ul>'))

--     -- return tabset
--     return pandoc.Div({
--       pandoc.Plain(nav),
--       panes
--     }, div.attr:clone())

--   end 
-- end

function bootstrapTabs() 
  return {
    ulAttribs = function(tabsetid)
      return 'class="nav nav-tabs" role="tablist"'
    end,
    liAttribs = function(tabid, isActive)
      return 'class="nav-item" role="presentation"'
    end,
    liLinkAttribs = function(tabid, isActive)
      local tablinkid = tabid .. "-tab"
      local active = ""
      local selected = "false"
      if isActive then
        active = " active"
        selected = "true"
      end
      return 'class="nav-link' .. active .. '" id="' .. tablinkid .. '" data-bs-toggle="tab" data-bs-target="#' .. tabid .. '" role="tab" aria-controls="' .. tabid .. '" aria-selected="' .. selected .. '"'
    end,
    paneAttribs = function(tabid, isActive, headingAttribs)
      local tablinkid = tabid .. "-tab"
      local attribs = headingAttribs:clone()
      attribs.identifier = tabid
      attribs.classes:insert("tab-pane")
      if isActive then
        attribs.classes:insert("active")
      end
      attribs.attributes["role"] = "tabpanel"
      attribs.attributes["aria-labelledby"] = tablinkid
      return attribs
    end
  }
end

function tabbyTabs()
  return {
    ulAttribs = function(tabsetid)
      return 'id="' .. tabsetid .. '" class="panel-tabset-tabby"'
    end,
    liAttribs = function(tabid, isActive)
      return ''
    end,
    liLinkAttribs = function(tabid, isActive)
      local default = ""
      if isActive then
        default = "data-tabby-default "
      end
      return default .. 'href="#' .. tabid .. '"'
    end,
    paneAttribs = function(tabid, isActive, headingAttribs)
      local attribs = headingAttribs:clone()
      attribs.identifier = tabid
      return attribs
    end
  }
end

local function min(a, b)
  if a < b then
    return a
  else
    return b
  end
end

function render_tabset_with_l4_headings(tabs)
  local result = pandoc.List()
  for i=1,#tabs do
    local tab = tabs[i]
    local heading = tab.content[1]
    local level = heading.level
    tab.content:remove(1)
    local tabid = "tab-" .. i
    result:insert(pandoc.Header(min(4, level), heading.content, heading.attr))
    result:extend(tab.content)
  end
  return result
end

-- function tabsetLatex(div_content)
--   -- find the first heading in the tabset
--   local heading = div_content:find_if(function(el) return el.t == "Header" end)
--   if heading ~= nil then
--     local level = heading.level
--     if level < 4 then
--       heading.level = 4

--       for i=1,#div_content do 
--         local el = div_content[i]
--         if el.t == "Header" and el.level == level then
--           el.level = 4
--         end
--       end 
--     end
--   end

--   return div_content
-- end
-- project_paths.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


kProjectResolverIgnore = 'project-resolve-ignore'

local function resolveProjectPath(path)
  local offset = _quarto.projectOffset()
  if offset and path and startsWith(path, '/') then
    return pandoc.path.join({offset, pandoc.text.sub(path, 2, #path)})
  else
    return nil
  end
end

-- resources that have '/' prefixed paths are treated as project
-- relative paths if there is a project context. For HTML output, 
-- these elements are dealt with in a post processor in website-resources.ts:resolveTag()
-- but for non-HTML output, we fix these here.
function projectPaths()
  return {
    Image = function(el)
      if el.attr.attributes[kProjectResolverIgnore] then
        el.attr.attributes[kProjectResolverIgnore] = ''
        return el
      end

      local resolved = false

      -- Resolve the image source
      if el.src then
        local resolvedPath = resolveProjectPath(el.src)
        if resolvedPath ~= nil then
          el.src = resolvedPath;
          resolved = true
        end
      end

      -- Resolve image data-src
      if el.attributes['data-src'] then
        local resolvedPath = resolveProjectPath(el.src)
        if resolvedPath ~= nil then
          el.attributes['data-src'] = resolvedPath;
          resolved = true
        end
      end

      if resolved then
        return el
      end
    end,

    Link = function(el)
      if el.attr.attributes[kProjectResolverIgnore] then
        el.attr.attributes[kProjectResolverIgnore] = ''
        return el
      end

      if el.href then
        local resolvedHref = resolveProjectPath(el.href)
        if resolvedHref then
          el.href = resolvedHref
          return el
        end
      end
    end
  }
end


-- resourcefiles.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

function resourceFiles() 
  return {
    -- TODO: discover resource files
    -- Note that currently even if we discover resourceFiles in markdown they don't 
    -- actually register for site preview b/c we don't actually re-render html
    -- files for preview if they are newer than the source files. we may need to
    -- record discovered resource files in some sort of index in order to work 
    -- around this
    Image = function(el)
      local targetPath = el.src
      if not targetPath:match('^https?:') and not targetPath:match('^data:') then
        -- don't include this resource if it is a URL, data file or some not file path
        if pandoc.path.is_relative(targetPath) then 
          local inputDir = pandoc.path.directory(quarto.doc.input_file)
          targetPath = pandoc.path.join({inputDir, el.src})
        end
        recordFileResource(el.src)
      end
    end,
  }
end

-- function to record a file resource
function recordFileResource(res)
  preState.results.resourceFiles:insert(res)
end


-- results.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


local function resultsFile()
  return pandoc.utils.stringify(param("results-file"))
end

local function timingsFile()
  return pandoc.utils.stringify(param("timings-file"))
end


-- write results
function writeResults()
  return {
    Pandoc = function(doc)
      local jsonResults = quarto.json.encode(preState.results)
      local rfile = io.open(resultsFile(), "w")
      if rfile then
        rfile:write(jsonResults)
        rfile:close()
      else
        warn('Error writing LUA results file')
      end

      if os.getenv("QUARTO_PROFILER_OUTPUT") ~= nil then

        local jsonTimings = quarto.json.encode(timing_events)
        local tfile = io.open(timingsFile(), "w")
        if tfile then
          tfile:write(jsonTimings)
          tfile:close()
        else
          warn('Error writing profiler timings JSON')
        end
      end
    end
  }
end

-- shortcodes-handlers.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- handlers process shortcode into either a list of inlines or into a list of blocks
   
local function shortcodeMetatable(scriptFile) 
  return {
    -- https://www.lua.org/manual/5.3/manual.html#6.1
    assert = assert,
    collectgarbage = collectgarbage,
    dofile = dofile,
    error = error,
    getmetatable = getmetatable,
    ipairs = ipairs,
    load = load,
    loadfile = loadfile,
    next = next,
    pairs = pairs,
    pcall = pcall,
    print = print,
    rawequal = rawequal,
    rawget = rawget,
    rawlen = rawlen,
    rawset = rawset,
    select = select,
    setmetatable = setmetatable,
    tonumber = tonumber,
    tostring = tostring,
    type = type,
    _VERSION = _VERSION,
    xpcall = xpcall,
    coroutine = coroutine,
    require = require,
    package = package,
    string = string,
    utf8 = utf8,
    table = table,
    math = math,
    io = io,
---@diagnostic disable-next-line: undefined-global
    file = file,
    os = os,
    debug = debug,
    -- https://pandoc.org/lua-filters.html
    FORMAT = FORMAT,
    PANDOC_READER_OPTIONS = PANDOC_READER_OPTIONS,
    PANDOC_WRITER_OPTIONS = PANDOC_WRITER_OPTIONS,
    PANDOC_VERSION = PANDOC_VERSION,
    PANDOC_API_VERSION = PANDOC_API_VERSION,
    PANDOC_SCRIPT_FILE = scriptFile,
    PANDOC_STATE = PANDOC_STATE,
    pandoc = pandoc,
    lpeg = lpeg,
    re = re,
    -- quarto functions
    quarto = quarto
  }
end

local handlers = {}

function initShortcodeHandlers()

  -- user provided handlers
  local shortcodeFiles = pandoc.List(param("shortcodes", {}))
  for _,shortcodeFile in ipairs(shortcodeFiles) do
    local env = setmetatable({}, {__index = shortcodeMetatable(shortcodeFile)})
    local chunk, err = loadfile(shortcodeFile, "bt", env)
    if chunk ~= nil and not err then
      local result = chunk()
      if result then
        for k,v in pairs(result) do
          handlers[k] = {
            file = shortcodeFile,
            handle = v
          }
        end
      else
        for k,v in pairs(env) do
          handlers[k] = {
            file = shortcodeFile,
            handle = v
          }
        end
      end
    else
      error(err)
      os.exit(1)
    end
  end


  -- built in handlers (these override any user handlers)
  handlers['meta'] = { handle = handleMeta }
  handlers['var'] = { handle = handleVars }
  handlers['env'] = { handle = handleEnv }
  handlers['pagebreak'] = { handle = handlePagebreak }
end

function handlerForShortcode(shortCode)
  return handlers[shortCode.name]
end


-- Implements reading values from envrionment variables
function handleEnv(args)
  if #args > 0 then
    -- the args are the var name
    local varName = inlinesToString(args[1])

    -- read the environment variable
    local envValue = os.getenv(varName)
    if envValue ~= nil then
      return { pandoc.Str(envValue) }  
    else 
      warn("Unknown variable " .. varName .. " specified in an env Shortcode.")
      return { pandoc.Strong({pandoc.Str("?env:" .. varName)}) } 
    end
  else
    -- no args, we can't do anything
    return nil
  end
end

-- Implements reading values from document metadata
-- as {{< meta title >}}
-- or {{< meta key.subkey.subkey >}}
-- This only supports emitting simple types (not arrays or maps)
function handleMeta(args) 
  if #args > 0 then
    -- the args are the var name
    local varName = inlinesToString(args[1])

    -- read the option value
    local optionValue = option(varName, nil)
    if optionValue ~= nil then
      return processValue(optionValue, varName, "meta")
    else 
      warn("Unknown meta key " .. varName .. " specified in a metadata Shortcode.")
      return { pandoc.Strong({pandoc.Str("?meta:" .. varName)}) } 
    end
  else
    -- no args, we can't do anything
    return nil
  end
end

-- Implements reading variables from quarto vars file
-- as {{< var title >}}
-- or {{< var key.subkey.subkey >}}
-- This only supports emitting simple types (not arrays or maps)
function handleVars(args) 
  if #args > 0 then
    
    -- the args are the var name
    local varName = inlinesToString(args[1])
    
    -- read the option value
    local varValue = var(varName, nil)
    if varValue ~= nil then
      return processValue(varValue, varName, "var")
    else 
      warn("Unknown var " .. varName .. " specified in a var shortcode.")
      return { pandoc.Strong({pandoc.Str("?var:" .. varName)}) } 
    end

  else
    -- no args, we can't do anything
    return nil
  end
end

function processValue(val, name, t)    
  if type(val) == "table" then
    if #val == 0 then
      return { pandoc.Str( "") }
    elseif pandoc.utils.type(val) == "Inlines" then
      return val
    elseif pandoc.utils.type(val) == "Blocks" then
      return pandoc.utils.blocks_to_inlines(val)
    elseif pandoc.utils.type(val) == "List" and #val == 1 then
      return processValue(val[1])
    else
      warn("Unsupported type '" .. pandoc.utils.type(val)  .. "' for key " .. name .. " in a " .. t .. " shortcode.")
      return { pandoc.Strong({pandoc.Str("?invalid " .. t .. " type:" .. name)}) }         
    end
  else 
    return { pandoc.Str( tostring(val) ) }  
  end
end


function handlePagebreak()
 
  local pagebreak = {
    epub = '<p style="page-break-after: always;"> </p>',
    html = '<div style="page-break-after: always;"></div>',
    latex = '\\newpage{}',
    ooxml = '<w:p><w:r><w:br w:type="page"/></w:r></w:p>',
    odt = '<text:p text:style-name="Pagebreak"/>',
    context = '\\page'
  }

  if FORMAT == 'docx' then
    return pandoc.RawBlock('openxml', pagebreak.ooxml)
  elseif FORMAT:match 'latex' then
    return pandoc.RawBlock('tex', pagebreak.latex)
  elseif FORMAT:match 'odt' then
    return pandoc.RawBlock('opendocument', pagebreak.odt)
  elseif FORMAT:match 'html.*' then
    return pandoc.RawBlock('html', pagebreak.html)
  elseif FORMAT:match 'epub' then
    return pandoc.RawBlock('html', pagebreak.epub)
  elseif FORMAT:match 'context' then
    return pandoc.RawBlock('context', pagebreak.context)
  else
    -- fall back to insert a form feed character
    return pandoc.Para{pandoc.Str '\f'}
  end

end
-- shortcodes.lua
-- Copyright (C) 2020-2022 Posit Software, PBC


-- The open and close shortcode indicators
local kOpenShortcode = "{{<"
local kOpenShortcodeEscape = "/*"
local kCloseShortcode = ">}}"
local kCloseShortcodeEscape = "*/"

function shortCodesBlocks() 
  return {
    Blocks = transformShortcodeBlocks,
    CodeBlock =  transformShortcodeCode,
    RawBlock = transformShortcodeCode
  }
end

function shortCodesInlines() 

  return {
    Inlines = transformShortcodeInlines,
    Code = transformShortcodeCode,
    RawInline = transformShortcodeCode,
    Link = transformLink,
    Image = transformImage
  }
end

-- transforms shortcodes in link targets
function transformLink(el)
  local target = urldecode(el.target)
  local tranformed = transformString(target);
  if tranformed ~= nil then
    el.target = tranformed
    return el
  end
end

-- transforms shortcodes in img srcs
function transformImage(el)
  local target = urldecode(el.src)
  local tranformed = transformString(target);
  if tranformed ~= nil then
    el.src = tranformed
    return el
  end
end

-- transforms shortcodes inside code
function transformShortcodeCode(el)

  -- don't process shortcodes in code output from engines
  -- (anything in an engine processed code block was actually
  --  proccessed by the engine, so should be printed as is)
  if el.attr and el.attr.classes:includes("cell-code") then
    return
  end

  -- don't process shortcodes if they are explicitly turned off
  if el.attr and el.attr.attributes["shortcodes"] == "false" then
    return
  end
  
  -- process shortcodes
  local text = el.text:gsub("(%{%{%{*<)" ..  "(.-)" .. "(>%}%}%}*)", function(beginCode, code, endCode) 
    if #beginCode > 3 or #endCode > 3 then
      return beginCode:sub(2) .. code .. endCode:sub(1, #endCode-1)
    else
      -- see if any of the shortcode handlers want it (and transform results to plain text)
      local inlines = markdownToInlines(kOpenShortcode .. code .. kCloseShortcode)
      local transformed = transformShortcodeInlines(inlines, true)
      if transformed ~= nil then
        -- need to undo fancy quotes
        local str = ''
        for _,inline in ipairs(transformed) do
          if inline.t == "Quoted" then
            local quote = '"'
            if inline.quotetype == "SingleQuote" then
              quote = "'"
            end
            str = str .. quote .. inlinesToString(inline.content) .. quote
          else
            str = str .. pandoc.utils.stringify(inline)
          end
        end
        return str
      else
        return beginCode .. code .. endCode
      end
    end
  end)

  -- return new element if the text changd
  if text ~= el.text then
    el.text = text
    return el
  end
end

-- finds blocks that only contain a shortcode and processes them
function transformShortcodeBlocks(blocks) 
  local transformed = false
  local scannedBlocks = pandoc.List()
  
  for i,block in ipairs(blocks) do 
    -- inspect para and plain blocks for shortcodes
    if block.t == "Para" or block.t == "Plain" then

      -- if contents are only a shortcode, process and return
      local onlyShortcode = onlyShortcode(block.content)
      if onlyShortcode ~= nil then
        -- there is a shortcode here, process it and return the blocks
        local shortCode = processShortCode(onlyShortcode)
        local handler = handlerForShortcode(shortCode)
        if handler ~= nil then
          local transformedShortcode = callShortcodeHandler(handler, shortCode)
          if transformedShortcode ~= nil then
            tappend(scannedBlocks, shortcodeResultAsBlocks(transformedShortcode, shortCode.name))
            transformed = true                  
          end
        else
          scannedBlocks:insert(block)
        end
      else 
        scannedBlocks:insert(block)
      end
    else
      scannedBlocks:insert(block)
    end
  end
  
  -- if we didn't transform any shortcodes, just return nil to signal
  -- no changes
  if transformed then
    return scannedBlocks
  else
    return nil
  end
end

-- helper function to read metadata options
local function readMetadata(value)
  -- We were previously coercing everything to lists of inlines when possible
  -- which made for some simpler treatment of values in meta, but it also
  -- meant that reading meta here was different than reading meta in filters
  -- 
  -- This is now just returning the raw meta value and not coercing it, so 
  -- users will have to be more thoughtful (or just use pandoc.utils.stringify)
  --
  -- Additionally, this used to return an empty list of inlines but now
  -- it returns nil for an unset value
  return option(value, nil)
end

-- call a handler w/ args & kwargs
function callShortcodeHandler(handler, shortCode)
  local args = pandoc.List()
  local kwargs = setmetatable({}, { __index = function () return pandoc.Inlines({}) end })
  for _,arg in ipairs(shortCode.args) do
    if arg.name then
      kwargs[arg.name] = arg.value
    else
      args:insert(arg.value)
    end
  end
  local meta = setmetatable({}, { __index = function(t, i) 
    return readMetadata(i)
  end})

  local callback = function()
    return handler.handle(args, kwargs, meta, shortCode.raw_args)
  end
  -- set the script file path, if present
  if handler.file ~= nil then
    return _quarto.withScriptFile(handler.file, callback)
  else
    return callback()
  end
end

-- scans through a list of inlines, finds shortcodes, and processes them
function transformShortcodeInlines(inlines, noRawInlines)
  local transformed = false
  local outputInlines = pandoc.List()
  local shortcodeInlines = pandoc.List()
  local accum = outputInlines

  local function ensure_accum(i)
    if not transformed then
      transformed = true
      for j = 1,i - 1 do
        outputInlines:insert(inlines[j])
      end
    end
  end
  
  -- iterate through any inlines and process any shortcodes
  for i, el in ipairs(inlines) do

    if el.t == "Str" then 

      -- find escaped shortcodes
      local beginEscapeMatch = el.text:match("%{%{%{+<$")
      local endEscapeMatch = el.text:match("^>%}%}%}+")

      -- handle {{{< shortcode escape
      if beginEscapeMatch then
        ensure_accum(i)
        local prefixLen = #el.text - #beginEscapeMatch
        if prefixLen > 0 then
          accum:insert(pandoc.Str(el.text:sub(1, prefixLen)))
        end
        accum:insert(pandoc.Str(beginEscapeMatch:sub(2)))
        
      -- handle >}}} shortcode escape
      elseif endEscapeMatch then
        ensure_accum(i)
        local suffixLen = #el.text - #endEscapeMatch
        accum:insert(pandoc.Str(endEscapeMatch:sub(1, #endEscapeMatch-1)))
        if suffixLen > 0 then
          accum:insert(pandoc.Str(el.text:sub(#endEscapeMatch + 1)))
        end

      -- handle shortcode escape -- e.g. {{</* shortcode_name */>}}
      elseif endsWith(el.text, kOpenShortcode .. kOpenShortcodeEscape) then
        -- This is an escape, so insert the raw shortcode as text (remove the comment chars)
        ensure_accum(i)
        accum:insert(pandoc.Str(kOpenShortcode))
        

      elseif startsWith(el.text, kCloseShortcodeEscape .. kCloseShortcode) then 
        -- This is an escape, so insert the raw shortcode as text (remove the comment chars)
        ensure_accum(i)
        accum:insert(pandoc.Str(kCloseShortcode))

      elseif endsWith(el.text, kOpenShortcode) then
        ensure_accum(i)
        -- note that the text might have other text with it (e.g. a case like)
        -- This is my inline ({{< foo bar >}}).
        -- Need to pare off prefix and suffix and preserve them
        local prefix = el.text:sub(1, #el.text - #kOpenShortcode)
        if prefix then
          accum:insert(pandoc.Str(prefix))
        end

        -- the start of a shortcode, start accumulating the shortcode
        accum = shortcodeInlines
        accum:insert(pandoc.Str(kOpenShortcode))
      elseif startsWith(el.text, kCloseShortcode) then

        -- since we closed a shortcode, mark this transformed
        ensure_accum(i)

        -- the end of the shortcode, stop accumulating the shortcode
        accum:insert(pandoc.Str(kCloseShortcode))
        accum = outputInlines

        -- process the shortcode
        local shortCode = processShortCode(shortcodeInlines)

        -- find the handler for this shortcode and transform
        local handler = handlerForShortcode(shortCode)
        if handler ~= nil then
          local expanded = callShortcodeHandler(handler, shortCode)
          if expanded ~= nil then
            -- process recursively
            expanded = shortcodeResultAsInlines(expanded, shortCode.name)
            local expandedAgain = transformShortcodeInlines(expanded, noRawInlines)
            if (expandedAgain ~= nil) then
              tappend(accum, expandedAgain)
            else
              tappend(accum, expanded)
            end
          end
        else
          if noRawInlines then
            tappend(accum, shortcodeInlines)
          else
            accum:insert(pandoc.RawInline("markdown", inlinesToString(shortcodeInlines)))
          end
        end

        local suffix = el.text:sub(#kCloseShortcode + 1)
        if suffix then
          accum:insert(pandoc.Str(suffix))
        end   

        -- clear the accumulated shortcode inlines
        shortcodeInlines = pandoc.List()        
      else 
        -- not a shortcode, accumulate
        if transformed then
          accum:insert(el)
        end
      end
    else
      -- not a string, accumulate
      if transformed then
        accum:insert(el)
      end
    end
  end
  
  if transformed then
    return outputInlines
  else
    return nil
  end

end

-- transforms shorts in a string
function transformString(str)
  if string.find(str, kOpenShortcode) then
    local inlines = markdownToInlines(str)
    if inlines ~= nil and #inlines > 0 then 
      local mutatedTarget = transformShortcodeInlines(inlines)
      if mutatedTarget ~= nil then
        return inlinesToString(mutatedTarget)
      end      
    end
  end  
  return nil
end

-- processes inlines into a shortcode data structure
function processShortCode(inlines) 

  local kSep = "="
  local shortCode = nil
  local args = pandoc.List()
  local raw_args = pandoc.List()

  -- slice off the open and close tags
  inlines = tslice(inlines, 2, #inlines - 1)

  -- handling for names with accompanying values
  local pendingName = nil
  notePendingName = function(el)
    pendingName = el.text:sub(1, -2)
  end

  -- Adds an argument to the args list (either named or unnamed args)
  insertArg = function(argInlines) 
    if pendingName ~= nil then
      -- there is a pending name, insert this arg
      -- with that name
      args:insert(
        {
          name = pendingName,
          value = argInlines
        })
      pendingName = nil
      raw_args:insert(argInlines)
    else
      -- split the string on equals
      if #argInlines == 1 and argInlines[1].t == "Str" and string.match(argInlines[1].text, kSep) then 
        -- if we can, split the string and assign name / value arg
        -- otherwise just put the whole thing in unnamed
        local parts = split(argInlines[1].text, kSep)
        if #parts == 2 then 
          args:insert(
              { 
                name = parts[1], 
                value = stringToInlines(parts[2])
              })
        else
          args:insert(
            { 
              value = argInlines 
            })
        end
        raw_args:insert(argInlines)
      -- a standalone SoftBreak or LineBreak is not an argument!
      -- (happens when users delimit args with newlines)
      elseif #argInlines > 1 or 
             (argInlines[1].t ~= "SoftBreak" and argInlines[1].t ~= "LineBreak") then
        -- this is an unnamed argument
        args:insert(
          { 
            value = argInlines
          })
        raw_args:insert(argInlines)
      end
    end
  end

  -- The core loop
  for i, el in ipairs(inlines) do
    if el.t == "Str" then
      if shortCode == nil then
        -- the first value is a pure text code name
        shortCode = el.text
      else
        -- if we've already captured the code name, proceed to gather args
        if endsWith(el.text, kSep) then 
          -- this is the name of an argument
          notePendingName(el)
        else
          -- this is either an unnamed arg or an arg value
          insertArg({el})
        end
      end
    elseif el.t == "Quoted" then 
      -- this is either an unnamed arg or an arg value
      insertArg(el.content)
    elseif el.t ~= "Space" then
      insertArg({el})
    end
  end

  return {
    args = args,
    raw_args = raw_args,
    name = shortCode
  }
end

function onlyShortcode(contents)
  
  -- trim leading and trailing empty strings
  contents = trimEmpty(contents)

  if #contents < 1 then
    return nil
  end

  -- starts with a shortcode
  local startsWithShortcode = contents[1].t == "Str" and contents[1].text == kOpenShortcode
  if not startsWithShortcode then
    return nil
  end

  -- ends with a shortcode
  local endsWithShortcode = contents[#contents].t == "Str" and contents[#contents].text == kCloseShortcode
  if not endsWithShortcode then  
    return nil
  end

  -- has only one open shortcode
  local openShortcodes = filter(contents, function(el) 
    return el.t == "Str" and el.text == kOpenShortcode  
  end)
  if #openShortcodes ~= 1 then
    return nil
  end

  -- has only one close shortcode 
  local closeShortcodes = filter(contents, function(el) 
    return el.t == "Str" and el.text == kCloseShortcode  
  end) 
  if #closeShortcodes ~= 1 then
    return nil
  end
    
  return contents
end

function trimEmpty(contents) 
  local firstNonEmpty = 1
  for i, el in ipairs(contents) do
    if el.t == "Str" and el.text == "" then
      firstNonEmpty = firstNonEmpty + 1
    else
      break
    end
  end
  if firstNonEmpty > 1 then
    contents = tslice(contents, firstNonEmpty, #contents)
  end

  local lastNonEmptyEl = nil
  for i = #contents, 1, -1 do
    el = contents[i]
    if el.t == "Str" and el.text == "" then
      contents = tslice(contents, 1, #contents - 1)
    else
      break
    end
  end
  return contents
end


function shortcodeResultAsInlines(result, name)
  local type = quarto.utils.type(result)
  if type == "CustomBlock" then
    error("Custom AST Block returned from shortcode, but Inline was expected")
    os.exit(1)
  elseif type == "CustomInline" then
    return pandoc.Inlines( { result })
  elseif type == "Inlines" then
    return result
  elseif type == "Blocks" then
    return pandoc.utils.blocks_to_inlines(result, { pandoc.Space() })
  elseif type == "string" then
    return pandoc.Inlines( { pandoc.Str(result) })
  elseif tisarray(result) then
    local items = pandoc.List(result)
    local inlines = items:filter(isInlineEl)
    if #inlines > 0 then
      return pandoc.Inlines(inlines)
    else
      local blocks = items:filter(isBlockEl)
      return pandoc.utils.blocks_to_inlines(blocks, { pandoc.Space() })
    end
  elseif isInlineEl(result) then
    return pandoc.Inlines( { result })
  elseif isBlockEl(result) then
    return pandoc.utils.blocks_to_inlines( { result }, { pandoc.Space() })
  else
    error("Unexepected result from shortcode " .. name .. "")
    quarto.log.output(result)
    os.exit(1)
  end
end
  
function shortcodeResultAsBlocks(result, name)
  local type = quarto.utils.type(result)

  if type == "CustomBlock" or type == "CustomInline" then
    return pandoc.Blocks({pandoc.Plain(result)})
  elseif type == "Blocks" then
    return result
  elseif type == "Inlines" then
    return pandoc.Blocks( {pandoc.Para(result) })
  elseif type == "string" then
    return pandoc.Blocks( {pandoc.Para({pandoc.Str(result)})} )
  elseif tisarray(result) then
    local items = pandoc.List(result)
    local blocks = items:filter(isBlockEl)
    if #blocks > 0 then
      return pandoc.Blocks(blocks)
    else
      local inlines = items:filter(isInlineEl)
      return pandoc.Blocks({pandoc.Para(inlines)})
    end
  elseif isBlockEl(result) then
    return pandoc.Blocks( { result } )
  elseif isInlineEl(result) then
    return pandoc.Blocks( {pandoc.Para( {result} ) })
  else
    error("Unexepected result from shortcode " .. name .. "")
    quarto.log.output(result)
    os.exit(1)
  end
end
-- table-classes.lua
-- Copyright (C) 2020-2023 Posit Software, PBC

-- handle classes to pass to `<table>` element
function tableClasses()

  return {
   
    Table = function(tbl)
      
      -- if there is no caption then return tbl unchanged
      if tbl.caption.long == nil or #tbl.caption.long < 1 then
        return tbl
      end

      -- generate a table containing recognized Bootstrap table classes
      local table_bootstrap_nm = {
        "primary", "secondary", "success", "danger", "warning", "info", "light", "dark",
        "striped", "hover", "active", "bordered", "borderless", "sm",
        "responsive", "responsive-sm", "responsive-md", "responsive-lg", "responsive-xl", "responsive-xxl"
      }

      -- determine if we have any supplied classes, these should always begin with a `.` and
      -- consist of alphanumeric characters
      local caption = tbl.caption.long[#tbl.caption.long]

      local caption_parsed, attr = parseTableCaption(pandoc.utils.blocks_to_inlines({caption}))

      local normalize_class = function(x)
        if tcontains(table_bootstrap_nm, x) then
          return "table-" .. x
        else
          return x
        end
      end
      local normalized_classes = attr.classes:map(normalize_class)

      -- ensure that classes are appended (do not want to rewrite and wipe out any existing)
      tbl.classes:extend(normalized_classes)

      -- if we have a `sm` table class then we need to add the `small` class
      -- and if we have a `small` class then we need to add the `table-sm` class
      if tcontains(normalized_classes, "table-sm") then
        tbl.classes:insert("small")
      elseif tcontains(normalized_classes, "small") then
        tbl.classes:insert("table-sm")
      end

      attr.classes = pandoc.List()

      tbl.caption.long[#tbl.caption.long] = pandoc.Plain(createTableCaption(caption_parsed, attr))

      return tbl
    end
  }

end
-- table-captions.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

kTblCap = "tbl-cap"
kTblSubCap = "tbl-subcap"

local latexCaptionPattern =  "(\\caption{)(.-)(}[^\n]*\n)"

function longtable_no_caption_fixup()
  return {
    RawBlock = function(raw)
      if _quarto.format.isRawLatex(raw) then
        if (raw.text:match(_quarto.patterns.latexLongtablePattern) and
            not raw.text:match(latexCaptionPattern)) then
          raw.text = raw.text:gsub(
            _quarto.patterns.latexLongtablePattern, "\\begin{longtable*}%2\\end{longtable*}", 1)
          return raw
        end
      end
    end
  }
end

function tableCaptions() 
  
  return {
   
    Div = function(el)
      if tcontains(el.attr.classes, "cell") then
        -- extract table attributes
        local tblCap = extractTblCapAttrib(el,kTblCap)
        local tblSubCap = extractTblCapAttrib(el, kTblSubCap, true)
        if hasTableRef(el) or tblCap then
          local tables = countTables(el)
          if tables > 0 then
           
            -- apply captions and labels if we have a tbl-cap or tbl-subcap
            if tblCap or tblSubCap then
  
              -- special case: knitr::kable will generate a \begin{tablular} without
              -- a \begin{table} wrapper -- put the wrapper in here if need be
              if _quarto.format.isLatexOutput() then
                el = _quarto.ast.walk(el, {
                  RawBlock = function(raw)
                    if _quarto.format.isRawLatex(raw) then
                      if raw.text:match(_quarto.patterns.latexTabularPattern) and not raw.text:match(_quarto.patterns.latexTablePattern) then
                        raw.text = raw.text:gsub(_quarto.patterns.latexTabularPattern, 
                                                "\\begin{table}\n\\centering\n%1%2%3\n\\end{table}\n",
                                                1)
                        return raw                       
                      end
                    end
                  end
                })
              end
  
              -- compute all captions and labels
              local label = el.attr.identifier
              local mainCaption, tblCaptions, mainLabel, tblLabels = tableCaptionsAndLabels(
                label,
                tables,
                tblCap,
                tblSubCap
              )              
              -- apply captions and label
              el.attr.identifier = mainLabel
              if mainCaption then
                el.content:insert(pandoc.Para(mainCaption))
              end
              if #tblCaptions > 0 then
                el = applyTableCaptions(el, tblCaptions, tblLabels)
              end
              return el
            end
          end
        end
      end
      
      
    end
  }

end

function tableCaptionsAndLabels(label, tables, tblCap, tblSubCap)
  
  local mainCaption = nil
  local tblCaptions = pandoc.List()
  local mainLabel = ""
  local tblLabels = pandoc.List()

  -- case: no subcaps (no main caption or label, apply caption(s) to tables)
  if not tblSubCap then
    -- case: single table (no label interpolation)
    if tables == 1 then
      tblCaptions:insert(markdownToInlines(tblCap[1]))
      tblLabels:insert(label)
    -- case: single caption (apply to entire panel)
    elseif #tblCap == 1 then
      mainCaption = tblCap[1]
      mainLabel = label
    -- case: multiple tables (label interpolation)
    else
      for i=1,tables do
        if i <= #tblCap then
          tblCaptions:insert(markdownToInlines(tblCap[i]))
          if #label > 0 then
            tblLabels:insert(label .. "-" .. tostring(i))
          else
            tblLabels:insert("")
          end
        end
      end
    end
  
  -- case: subcaps
  else
    mainLabel = label
    if mainLabel == "" then
      mainLabel = anonymousTblId()
    end
    if tblCap then
      mainCaption = markdownToInlines(tblCap[1])
    else
      mainCaption = noCaption()
    end
    for i=1,tables do
      if tblSubCap and i <= #tblSubCap and tblSubCap[i] ~= "" then
        tblCaptions:insert(markdownToInlines(tblSubCap[i]))
      else
        tblCaptions:insert(pandoc.List())
      end
      if #mainLabel > 0 then
        tblLabels:insert(mainLabel .. "-" .. tostring(i))
      else
        tblLabels:insert("")
      end
    end
  end

  return mainCaption, tblCaptions, mainLabel, tblLabels

end

function applyTableCaptions(el, tblCaptions, tblLabels)
  local idx = 1
  return _quarto.ast.walk(el, {
    Table = function(el)
      if idx <= #tblLabels then
        local cap = pandoc.Inlines({})
        if #tblCaptions[idx] > 0 then
          cap:extend(tblCaptions[idx])
          cap:insert(pandoc.Space())
        end
        if #tblLabels[idx] > 0 then
          cap:insert(pandoc.Str("{#" .. tblLabels[idx] .. "}"))
        end
        idx = idx + 1
        el.caption.long = pandoc.Plain(cap)
        return el
      end
    end,
    RawBlock = function(raw)
      if idx <= #tblLabels then
        -- (1) if there is no caption at all then populate it from tblCaptions[idx]
        -- (assuming there is one, might not be in case of empty subcaps)
        -- (2) Append the tblLabels[idx] to whatever caption is there
        if hasRawHtmlTable(raw) then
          -- html table patterns
          local tablePattern = htmlTablePattern()
          local captionPattern = htmlTableCaptionPattern()
          -- insert caption if there is none
          local beginCaption, caption = raw.text:match(captionPattern)
          if not beginCaption then
            raw.text = raw.text:gsub(tablePattern, "%1" .. "<caption></caption>" .. "%2%3", 1)
          end
          -- apply table caption and label
          local beginCaption, captionText, endCaption = raw.text:match(captionPattern)
          if #tblCaptions[idx] > 0 then
            captionText = stringEscape(tblCaptions[idx], "html")
          end
          if #tblLabels[idx] > 0 then
            captionText = captionText .. " {#" .. tblLabels[idx] .. "}"
          end
          raw.text = raw.text:gsub(captionPattern, "%1" .. captionText:gsub("%%", "%%%%") .. "%3", 1)
          idx = idx + 1
        elseif hasRawLatexTable(raw) then
          for i,pattern in ipairs(_quarto.patterns.latexTablePatterns) do
            if raw.text:match(pattern) then
              raw.text = applyLatexTableCaption(raw.text, tblCaptions[idx], tblLabels[idx], pattern)
              break
            end
          end
          idx = idx + 1
        elseif hasPagedHtmlTable(raw) then
          if #tblCaptions[idx] > 0 then
            local captionText = stringEscape(tblCaptions[idx], "html")
            if #tblLabels[idx] > 0 then
              captionText = captionText .. " {#" .. tblLabels[idx] .. "}"
            end
            local pattern = "(<div data[-]pagedtable=\"false\">)"
            -- we don't have a table to insert a caption to, so we'll wrap the caption with a div and the right class instead
            local replacement = "%1 <div class=\"table-caption\"><caption>" .. captionText:gsub("%%", "%%%%") .. "</caption></div>"
            raw.text = raw.text:gsub(pattern, replacement)
          end
          idx = idx + 1
        end
       
        return raw
      end
    end
  })
end


function applyLatexTableCaption(latex, tblCaption, tblLabel, tablePattern)
  -- insert caption if there is none
  local beginCaption, caption = latex:match(latexCaptionPattern)
  if not beginCaption then
    latex = latex:gsub(tablePattern, "%1" .. "\n\\caption{ }\\tabularnewline\n" .. "%2%3", 1)
  end
  -- apply table caption and label
  local beginCaption, captionText, endCaption = latex:match(latexCaptionPattern)
  if #tblCaption > 0 then
    captionText = stringEscape(tblCaption, "latex")
  end
  if #tblLabel > 0 then
    captionText = captionText .. " {#" .. tblLabel .. "}"
  end
  latex = latex:gsub(latexCaptionPattern, "%1" .. captionText:gsub("%%", "%%%%") .. "%3", 1)
  return latex
end


function extractTblCapAttrib(el, name, subcap)
  local value = attribute(el, name, nil)
  if value then
    if startsWith(value, "[") then
      value = pandoc.List(quarto.json.decode(value))
    elseif subcap and (value == "true") then
      value = pandoc.List({ "" })
    else
      value = pandoc.List({ value })
    end
    el.attr.attributes[name] = nil
    return value
  end
  return nil
end
-- table-colwidth.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

local kTblColwidths = "tbl-colwidths"

local function noWidths(ncol)
  local widths = {}
  for i = 1,ncol do
    widths[i] = 0
  end
  return widths
end

-- takes a tblColwidths attribute value (including nil) and returns an table
-- of pandoc AST colwidths 
local function tblColwidthValues(tbl, tblColwidths)
  -- determine the widths (using any passed param as the default)
  if tblColwidths == nil then
    tblColwidths = param(kTblColwidths, true)
  elseif tblColwidths == "true" then
    tblColwidths = true
  elseif tblColwidths == "false" then
    tblColwidths = false
  end

  -- take appropriate action
  if tblColwidths == "auto" then
    local foundLink = false
    _quarto.ast.walk(tbl, {
      Link = function(el)
        foundLink = true
      end
    })
    if foundLink then
      return noWidths(#tbl.colspecs)
    else
      return nil
    end
  elseif tblColwidths == true then
    return nil
  elseif tblColwidths == false then
    return noWidths(#tbl.colspecs)
  else
    if type(tblColwidths) == "string" then
      -- provide array brackets if necessary
      if tblColwidths:find("[", 1, true) ~= 1 then
        tblColwidths = '[' .. tblColwidths .. ']'
      end
      -- decode array
      tblColwidths = quarto.json.decode(tblColwidths)
    end
    if type(tblColwidths) == "table" then
      local totalWidth = 0
      local widths = {}
      for i = 1,#tbl.colspecs do
        if i <= #tblColwidths then
          widths[i] = tblColwidths[i]
        else
          widths[i] = tblColwidths[#tblColwidths]
        end
        totalWidth = totalWidth + widths[i]
      end

      -- normalize to 100 if the total is > 100
      if totalWidth > 100 then
        for i=1,#widths do 
          widths[i] = round((widths[i]/totalWidth) * 100, 1)
        end
      end

      -- convert all widths to decimal
      for i=1,#widths do 
        widths[i] = round(widths[i] / 100, 2)
      end

      return widths
    else
      warn("Unexpected tbl-colwidths value: " .. tblColwidths)
      return nil
    end
  end
end

-- propagate cell level tbl-colwidths to tables
function tableColwidthCell() 
  return {
    Div = function(el)
      if tcontains(el.attr.classes, "cell") then
        local tblColwidths = el.attr.attributes[kTblColwidths]
        el.attr.attributes[kTblColwidths] = nil
        if tblColwidths ~= nil then
          return _quarto.ast.walk(el, {
            Table = function(tbl)
              tbl.attr.attributes[kTblColwidths] = tblColwidths
              return tbl
            end
          })
        end
      end
    end,
  }
end

-- handle tbl-colwidth
function tableColwidth()

  return {
   
    Table = function(tbl)
     
      -- see if we have a tbl-colwidths attribute
      local tblColwidths = nil
      if tbl.caption.long ~= nil and #tbl.caption.long > 0 then
        local caption =  tbl.caption.long[#tbl.caption.long]
        
        local tblCaption, attr = parseTableCaption(pandoc.utils.blocks_to_inlines({caption}))
        tblColwidths = attr.attributes[kTblColwidths]
        if tblColwidths ~= nil then
          attr.attributes[kTblColwidths] = nil
          tbl.caption.long[#tbl.caption.long] = pandoc.Plain(createTableCaption(tblCaption, attr))
        end
      end

      -- failing that check for an ambient attribute provided by a cell
      if tblColwidths == nil then
        tblColwidths = tbl.attr.attributes[kTblColwidths]
      end
      tbl.attr.attributes[kTblColwidths] = nil

      -- if we found a quarto-postprocess attribute,
      -- that means this was a table parsed from html and
      -- we don't need to do the fixups
      if tbl.attr.attributes["quarto-postprocess"] then
        return nil
      end
      
      -- realize values and apply them
      local colwidthValues = tblColwidthValues(tbl, tblColwidths)
      if colwidthValues ~= nil then
        local simpleTbl = pandoc.utils.to_simple_table(tbl)
        simpleTbl.widths = colwidthValues
        return pandoc.utils.from_simple_table(simpleTbl)
      end
    end
  }

end

-- table-rawhtml.lua
-- Copyright (C) 2020-2022 Posit Software, PBC

-- flextable outputs consecutive html blocks so we merge them
-- back together here so they can be processed by ourraw  table
-- caption handling
function tableMergeRawHtml()
  if _quarto.format.isHtmlOutput() then
    return {
      Blocks = function(blocks)
        local pendingRaw = ''
        local merged = pandoc.List()
        for i,el in ipairs(blocks) do
          if _quarto.format.isRawHtml(el) and el.text:find(htmlTableTagNamePattern()) then
            pendingRaw = pendingRaw .. "\n" .. el.text
          else
            if #pendingRaw > 0 then
              merged:insert(pandoc.RawBlock("html", pendingRaw))
              pendingRaw = ''
            end
            merged:insert(el)
          end
        end
        if #pendingRaw > 0 then
          merged:insert(pandoc.RawBlock("html", pendingRaw))
        end
        return merged
      end
    }
  else
    return {

    }
  end
end

-- re-emits GT's CSS with lower specificity
function respecifyGtCSS(text)
  local s, e, v = text:find('<div id="([a-z]+)"')
  -- if gt does not emit a div, do nothing
  if v == nil then
    return text
  end
  return text:gsub("\n#" .. v, "\n:where(#" .. v .. ")")
end

function tableRenderRawHtml() 
  return {
    RawBlock = function(el)
      if hasGtHtmlTable(el) then
        el.text = respecifyGtCSS(el.text)
      end
      if _quarto.format.isRawHtml(el) then
        -- if we have a raw html table in a format that doesn't handle raw_html
        -- then have pandoc parse the table into a proper AST table block
        if not _quarto.format.isHtmlOutput() and not _quarto.format.isMarkdownWithHtmlOutput() and not _quarto.format.isIpynbOutput() then
          local tableBegin,tableBody,tableEnd = el.text:match(htmlTablePattern())
          if tableBegin then
            local tableHtml = tableBegin .. "\n" .. tableBody .. "\n" .. tableEnd
            local tableDoc = pandoc.read(tableHtml, "html")
            return tableDoc.blocks
          end
        end
      end
      return el
    end
  }
end
-- theorems.lua
-- Copyright (C) 2021-2022 Posit Software, PBC


function quartoPreTheorems() 
  
  return {
    Div = function(el)
      if hasTheoremRef(el) then
        local capEl = el.content[1]
        if capEl ~= nil and capEl.t == 'Header' then
          capEl.attr.classes:insert("unnumbered")
          capEl.attr.classes:insert("unlisted")
        end
      end
      return el
    end,
  }
end
-- decoratedcodeblock.lua
-- Copyright (C) 2020-2023 Posit Software, PBC

-- A custom AST node for decorated code blocks
-- so we can render the decorations in the right order

_quarto.ast.add_handler({
  -- decorated code blocks can't be represented as divs in markdown, they can
  -- only be constructed directly in Lua
  class_name = {},

  -- the name of the ast node, used as a key in extended ast filter tables
  ast_name = "DecoratedCodeBlock",

  -- callouts will be rendered as blocks
  kind = "Block",

  -- a function that takes the div node as supplied in user markdown
  -- and returns the custom node
  parse = function(div)
    print("internal error, should not have arrived here")
    crash_with_stack_trace()
  end,

  -- a function that renders the extendedNode into output
  render = function(node)
    local el = node.code_block
    if _quarto.format.isHtmlOutput() then
      local filenameEl
      local caption
      local classes = pandoc.List()
      local fancy_output = false
      if node.filename ~= nil then
        filenameEl = pandoc.Div({pandoc.Plain{
          pandoc.RawInline("html", "<pre>"),
          pandoc.Strong{pandoc.Str(node.filename)},
          pandoc.RawInline("html", "</pre>")
        }}, pandoc.Attr("", {"code-with-filename-file"}))
        classes:insert("code-with-filename")
        fancy_output = true
      end
      if node.caption ~= nil then
        local order = node.order
        if order == nil then
          warn("Node with caption " .. pandoc.utils.stringify(node.caption) .. " is missing a listing id (lst-*).")
          warn("This usage is unsupported in HTML formats.")
          return el
        end
        local captionContent = node.caption
        tprepend(captionContent, listingTitlePrefix(order))
        caption = pandoc.Para(captionContent)
        classes:insert("listing")
        fancy_output = true
      end

      if not fancy_output then
        return el
      end

      local blocks = pandoc.Blocks({})
      if caption ~= nil then
        blocks:insert(caption)
      end
      if filenameEl ~= nil then
        blocks:insert(filenameEl)
      end
      blocks:insert(el)

      return pandoc.Div(blocks, pandoc.Attr("", classes))
    elseif _quarto.format.isLatexOutput() then
      -- add listing class to the code block
      el.attr.classes:insert("listing")

      -- if we are use the listings package we don't need to do anything
      -- further, otherwise generate the listing div and return it
      if not latexListings() then
        local listingDiv = pandoc.Div({})
        listingDiv.content:insert(pandoc.RawBlock("latex", "\\begin{codelisting}"))

        local captionContent = node.caption

        if node.filename ~= nil and captionContent ~= nil then
          -- with both filename and captionContent we need to add a colon
          local listingCaption = pandoc.Plain({pandoc.RawInline("latex", "\\caption{")})
          listingCaption.content:insert(
            pandoc.RawInline("latex", "\\texttt{" .. node.filename .. "}: ")
          )
          listingCaption.content:extend(captionContent)
          listingCaption.content:insert(pandoc.RawInline("latex", "}"))
          listingDiv.content:insert(listingCaption)
        elseif node.filename ~= nil and captionContent == nil then
          local listingCaption = pandoc.Plain({pandoc.RawInline("latex", "\\caption{")})
          -- with just filename we don't add a colon
          listingCaption.content:insert(
            pandoc.RawInline("latex", "\\texttt{" .. node.filename .. "}")
          )
          listingCaption.content:insert(pandoc.RawInline("latex", "}"))
          listingDiv.content:insert(listingCaption)
        elseif node.filename == nil and captionContent ~= nil then
          local listingCaption = pandoc.Plain({pandoc.RawInline("latex", "\\caption{")})
          listingCaption.content:extend(captionContent)
          listingCaption.content:insert(pandoc.RawInline("latex", "}"))
          listingDiv.content:insert(listingCaption)
        end

        listingDiv.content:insert(el)
        listingDiv.content:insert(pandoc.RawBlock("latex", "\\end{codelisting}"))
        return listingDiv
      end
      return el
    elseif _quarto.format.isMarkdownOutput() then
      -- see https://github.com/quarto-dev/quarto-cli/issues/5112
      -- 
      -- This is a narrow fix for the 1.3 regression.
      -- We still don't support listings output in markdown since that wasn't supported in 1.2 either.
      -- But that'll be done in 1.4 with crossrefs overhaul.

      if node.filename then
        -- if we have a filename, add it as a header
        return pandoc.Div(
          { pandoc.Plain{pandoc.Strong{pandoc.Str(node.filename)}}, el },
          pandoc.Attr("", {"code-with-filename"})
        )
      else
        return el
      end
    else
      -- return the code block unadorned
      -- this probably could be improved
      return el
    end
  end,

  inner_content = function(extended_node)
    return {}
  end,

  set_inner_content = function(extended_node, values)
    return extended_node
  end,

  constructor = function(tbl)
    local caption = tbl.caption
    if tbl.code_block.attributes["lst-cap"] ~= nil then
      caption = pandoc.read(tbl.code_block.attributes["lst-cap"], "markdown").blocks[1].content
    end
    return {
      filename = tbl.filename,
      order = tbl.order,
      caption = caption,
      code_block = tbl.code_block
    }
  end
})
-- main.lua
-- Copyright (C) 2020-2023 Posit Software, PBC

-- required version
PANDOC_VERSION:must_be_at_least '2.13'

crossref = {
  usingTheorems = false,
  startAppendix = nil
}



initCrossrefIndex()

initShortcodeHandlers()

local quartoInit = {
  { name = "init-configure-filters", filter = configureFilters() },
  { name = "init-readIncludes", filter = readIncludes() },
  { name = "init-metadataResourceRefs", filter = combineFilters({
    fileMetadata(),
    resourceRefs()
  })},
}

local quartoNormalize = {
  { name = "normalize", filter = filterIf(function()
    return preState.active_filters.normalization
  end, normalizeFilter()) },
  { name = "normalize-parseHtmlTables", filter = parse_html_tables() },
  { name = "normalize-extractQuartoDom", filter = extract_quarto_dom() },
  { name = "normalize-parseExtendedNodes", filter = parseExtendedNodes() }
}

local quartoPre = {
  -- quarto-pre
  { name = "pre-quartoBeforeExtendedUserFilters", filters = make_wrapped_user_filters("beforeQuartoFilters") },

  -- https://github.com/quarto-dev/quarto-cli/issues/5031
  -- recompute options object in case user filters have changed meta
  -- this will need to change in the future; users will have to indicate
  -- when they mutate options
  { name = "pre-quartoAfterUserFilters", filter = initOptions() },

  { name = "normalize-parse-pandoc3-figures", filter = parse_pandoc3_figures() },
  { name = "pre-bibliographyFormats", filter = bibliographyFormats() }, 
  { name = "pre-shortCodesBlocks", filter = shortCodesBlocks() } ,
  { name = "pre-shortCodesInlines", filter = shortCodesInlines() },
  { name = "pre-tableMergeRawHtml", filter = tableMergeRawHtml() },
  { name = "pre-tableRenderRawHtml", filter = tableRenderRawHtml() },
  { name = "pre-tableColwidthCell", filter = tableColwidthCell() },
  { name = "pre-tableColwidth", filter = tableColwidth() },
  { name = "pre-tableClasses", filter = tableClasses() },
  { name = "pre-hidden", filter = hidden() },
  { name = "pre-contentHidden", filter = contentHidden() },
  { name = "pre-tableCaptions", filter = tableCaptions() },
  { name = "pre-longtable_no_caption_fixup", filter = longtable_no_caption_fixup() },
  { name = "pre-code-annotations", filter = code()},
  { name = "pre-code-annotations-meta", filter = codeMeta()},
  { name = "pre-outputs", filter = outputs() },
  { name = "pre-outputLocation", filter = outputLocation() },
  { name = "pre-combined-figures-theorems-etc", filter = combineFilters({
    fileMetadata(),
    indexBookFileTargets(),
    bookNumbering(),
    includePaths(),
    resourceFiles(),
    quartoPreFigures(),
    quartoPreTheorems(),
    callout(),
    codeFilename(),
    lineNumbers(),
    engineEscape(),
    panelInput(),
    panelLayout(),
    panelSidebar(),
    inputTraits()
  }) },
  { name = "pre-combined-book-file-targets", filter = combineFilters({
    fileMetadata(),
    resolveBookFileTargets(),
  }) },
  { name = "pre-quartoPreMetaInject", filter = quartoPreMetaInject() },
  { name = "pre-writeResults", filter = writeResults() },
  { name = "pre-projectPaths", filter = projectPaths() }
}

local quartoPost = {
  -- quarto-post
  { name = "post-cell-cleanup", filter = cell_cleanup() },
  { name = "post-cites", filter = indexCites() },
  { name = "post-foldCode", filter = foldCode() },
  { name = "post-bibligraphy", filter = bibliography() },
  { name = "post-figureCleanupCombined", filter = combineFilters({
    latexDiv(),
    responsive(),
    ipynb(),
    quartoBook(),
    reveal(),
    tikz(),
    pdfImages(),
    delink(),
    figCleanup(),
    responsive_table(),
  }) },
  { name = "post-ojs", filter = ojs() },
  { name = "post-postMetaInject", filter = quartoPostMetaInject() },
  { name = "post-render-jats", filter = jats() },
  { name = "post-render-asciidoc", filter = renderAsciidoc() },
  { name = "post-renderExtendedNodes", filter = renderExtendedNodes() },
  { name = "post-render-pandoc-3-figures", filter = render_pandoc3_figures() },
  { name = "post-userAfterQuartoFilters", filters = make_wrapped_user_filters("afterQuartoFilters") },
}

local quartoFinalize = {
    -- quarto-finalize
    { name = "finalize-fileMetadataAndMediabag", filter =
    combineFilters({
      fileMetadata(),
      mediabag()
    })
  },
  { name = "finalize-bookCleanup", filter = bookCleanup() },
  { name = "finalize-cites", filter = writeCites() },
  { name = "finalize-metaCleanup", filter = metaCleanup() },
  { name = "finalize-dependencies", filter = dependencies() },
  { name = "finalize-wrapped-writer", filter = wrapped_writer() }
}

local quartoLayout = {
  { name = "layout-columnsPreprocess", filter = columnsPreprocess() },
  { name = "layout-columns", filter = columns() },
  { name = "layout-citesPreprocess", filter = citesPreprocess() },
  { name = "layout-cites", filter = cites() },
  { name = "layout-panels", filter = layoutPanels() },
  { name = "layout-extendedFigures", filter = extendedFigures() },
  { name = "layout-metaInject", filter = layoutMetaInject() }
}

local quartoCrossref = {
  { name = "crossref-initCrossrefOptions", filter = initCrossrefOptions() },
  { name = "crossref-preprocess", filter = crossrefPreprocess() },
  { name = "crossref-preprocessTheorems", filter = crossrefPreprocessTheorems() },
  { name = "crossref-combineFilters", filter = combineFilters({
    fileMetadata(),
    qmd(),
    sections(),
    crossrefFigures(),
    crossrefTables(),
    equations(),
    listings(),
    crossrefTheorems(),
  })},
  { name = "crossref-resolveRefs", filter = resolveRefs() },
  { name = "crossref-crossrefMetaInject", filter = crossrefMetaInject() },
  { name = "crossref-writeIndex", filter = writeIndex() },
}

local filterList = {}

tappend(filterList, quartoInit)
tappend(filterList, quartoNormalize)
tappend(filterList, quartoPre)
tappend(filterList, quartoCrossref)
tappend(filterList, quartoLayout)
tappend(filterList, quartoPost)
tappend(filterList, quartoFinalize)

local profiler = require("profiler")

local result = run_as_extended_ast({
  pre = {
    initOptions()
  },
  afterFilterPass = function() 
    -- After filter pass is called after each pass through a filter group
    -- allowing state or other items to be handled
    resetFileMetadata()
  end,
  filters = capture_timings(filterList),
})

return result

-- TODO!!
-- citeproc detection/toggle

--[[ from filters.ts:

// citeproc at the very end so all other filters can interact with citations
filters = filters.filter((filter) => filter !== kQuartoCiteProcMarker);
const citeproc = citeMethod(options) === kQuartoCiteProcMarker;
if (citeproc) {
  // If we're explicitely adding the citeproc filter, turn off
  // citeproc: true so it isn't run twice
  // See https://github.com/quarto-dev/quarto-cli/issues/2393
  if (options.format.pandoc.citeproc === true) {
    delete options.format.pandoc.citeproc;
  }

  quartoFilters.push(kQuartoCiteProcMarker);
}

]]
