texlive[72292] Master/texmf-dist: citation-style-language (15sep24)

commits+karl at tug.org commits+karl at tug.org
Sun Sep 15 21:57:10 CEST 2024


Revision: 72292
          https://tug.org/svn/texlive?view=revision&revision=72292
Author:   karl
Date:     2024-09-15 21:57:10 +0200 (Sun, 15 Sep 2024)
Log Message:
-----------
citation-style-language (15sep24)

Modified Paths:
--------------
    trunk/Master/texmf-dist/doc/latex/citation-style-language/CHANGELOG.md
    trunk/Master/texmf-dist/doc/latex/citation-style-language/citation-style-language-doc.pdf
    trunk/Master/texmf-dist/doc/latex/citation-style-language/citation-style-language-doc.tex
    trunk/Master/texmf-dist/doc/man/man1/citeproc-lua.1
    trunk/Master/texmf-dist/doc/man/man1/citeproc-lua.man1.pdf
    trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-bibtex-data.lua
    trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua
    trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-manager.lua
    trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-node-citation.lua
    trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua
    trunk/Master/texmf-dist/scripts/citation-style-language/citeproc.lua
    trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language-init.sty
    trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language.sty
    trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-AT.xml
    trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-CH.xml
    trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-DE.xml
    trunk/Master/texmf-dist/tex/latex/citation-style-language/styles/apa.csl

Modified: trunk/Master/texmf-dist/doc/latex/citation-style-language/CHANGELOG.md
===================================================================
--- trunk/Master/texmf-dist/doc/latex/citation-style-language/CHANGELOG.md	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/doc/latex/citation-style-language/CHANGELOG.md	2024-09-15 19:57:10 UTC (rev 72292)
@@ -7,6 +7,12 @@
 
 ## [Unreleased]
 
+## [0.6.4] - 2024-09-15
+
+### Fixed
+
+- Refactor `processCitationCluster()` to fix unexpected nil ([#77](https://github.com/zepinglee/citeproc-lua/issues/77)).
+
 ## [0.6.3] - 2024-08-28
 
 ### Added
@@ -241,7 +247,8 @@
 
 - Initial CTAN release.
 
-[Unreleased]: https://github.com/zepinglee/citeproc-lua/compare/v0.6.3...HEAD
+[Unreleased]: https://github.com/zepinglee/citeproc-lua/compare/v0.6.4...HEAD
+[0.6.4]: https://github.com/zepinglee/citeproc-lua/compare/v0.6.3...v0.6.4
 [0.6.3]: https://github.com/zepinglee/citeproc-lua/compare/v0.6.2...v0.6.3
 [0.6.2]: https://github.com/zepinglee/citeproc-lua/compare/v0.6.1...v0.6.2
 [0.6.1]: https://github.com/zepinglee/citeproc-lua/compare/v0.6.0...v0.6.1

Modified: trunk/Master/texmf-dist/doc/latex/citation-style-language/citation-style-language-doc.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/doc/latex/citation-style-language/citation-style-language-doc.tex
===================================================================
--- trunk/Master/texmf-dist/doc/latex/citation-style-language/citation-style-language-doc.tex	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/doc/latex/citation-style-language/citation-style-language-doc.tex	2024-09-15 19:57:10 UTC (rev 72292)
@@ -51,7 +51,7 @@
   }%
 }
 
-\date{2024-08-28 v0.6.3}
+\date{2024-09-15 v0.6.4}
 
 \maketitle
 

Modified: trunk/Master/texmf-dist/doc/man/man1/citeproc-lua.1
===================================================================
--- trunk/Master/texmf-dist/doc/man/man1/citeproc-lua.1	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/doc/man/man1/citeproc-lua.1	2024-09-15 19:57:10 UTC (rev 72292)
@@ -1,4 +1,4 @@
-.TH citeproc-lua 1 "0.6.3"
+.TH citeproc-lua 1 "0.6.4"
 .SH NAME
 citeproc-lua \- make CSL citations and bibliography for LaTeX
 .SH SYNOPSIS

Modified: trunk/Master/texmf-dist/doc/man/man1/citeproc-lua.man1.pdf
===================================================================
(Binary files differ)

Modified: trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-bibtex-data.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-bibtex-data.lua	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-bibtex-data.lua	2024-09-15 19:57:10 UTC (rev 72292)
@@ -1272,6 +1272,11 @@
       source = "apa.dbx",
       type = "literal",
     },
+    citations = {
+      csl = nil,
+      source = "publist.dbx",
+      type = "literal",
+    },
     citedate = {
       csl = nil,
       source = "seuthesis.bst",
@@ -4205,11 +4210,6 @@
       csl = nil,
       source = "adrbirthday.bst",
     },
-    citations = {
-      csl = nil,
-      source = "publist.dbx",
-      type = "literal",
-    },
   },
   macros = {
     acmcs = {

Modified: trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-engine.lua	2024-09-15 19:57:10 UTC (rev 72292)
@@ -59,7 +59,18 @@
 --- at field citationItems CitationItem[]
 --- at field properties CitationProperties
 --- at field citation_index integer
+--- at field sorted_items CitationItem[]
 
+--- at class CitationItem
+--- at field id CiteId
+--- at field prefix string?
+--- at field suffix string?
+--- at field locator string?
+--- at field label string?
+--- at field position_level Position?
+--- at field near_note boolean?
+--- at field num_citations integer?
+
 --- at class CitationProperties
 --- at field noteIndex NoteIndex,
 --- at field chapterIndex ChapterIndex,
@@ -114,7 +125,6 @@
 --- at field registry Registry
 --- at field cite_first_note_numbers table<ItemId, NoteIndex>
 --- at field cite_last_note_numbers table<ItemId, NoteIndex>
---- at field note_citations_map table<NoteIndex, CitationId[]>
 --- at field tainted_item_ids table<ItemId, boolean>
 --- at field disam_irs IrNode[]
 --- at field cite_irs_by_output table<string, IrNode[]>
@@ -177,7 +187,6 @@
 
     cite_first_note_numbers = {},
     cite_last_note_numbers = {},
-    note_citations_map = {},
 
     tainted_item_ids = {},
 
@@ -212,16 +221,6 @@
   return self.style.info.independent_parent
 end
 
-function CiteProc:check_valid_citation_element()
-  if not self.style.citation then
-    if self.style.info and self.style.info.independent_parent then
-      util.error(string.format("This is a dependent style linked to '%s'.", self.style.info.independent_parent))
-    else
-      util.error('No <citation> in style.')
-    end
-  end
-end
-
 --- at param ids CiteId[]
 function CiteProc:updateItems(ids)
   -- util.debug(string.format('updateItems(%s)', table.concat(ids, ", ")))
@@ -257,7 +256,6 @@
   self.registry.previous_citation = nil
   self.cite_first_note_numbers = {}
   self.cite_last_note_numbers = {}
-  self.note_citations_map = {}
 
   for _, item in ipairs(self.registry.registry) do
     item.year_suffix_number = nil
@@ -306,106 +304,214 @@
   self.registry.previous_citation = nil
   self.cite_first_note_numbers = {}
   self.cite_last_note_numbers = {}
-  self.note_citations_map = {}
 end
 
 
---- at alias PreCitation [CitationId, NoteIndex, ChapterIndex]
---- at alias PostCitation [CitationId, NoteIndex, ChapterIndex]
+--- at alias PreCitation [CitationId, NoteIndex, ChapterIndex?]
+--- at alias PostCitation [CitationId, NoteIndex, ChapterIndex?]
 
 --- at param citation CitationData
---- at param citationsPre PreCitation[]
---- at param citationsPost PostCitation[]
---- at return (table | (integer | string | CitationId)[])[]
-function CiteProc:processCitationCluster(citation, citationsPre, citationsPost)
+--- at param citations_pre PreCitation[]
+--- at param citations_post PostCitation[]
+--- at return [table, [integer, string, CitationId][]]
+function CiteProc:processCitationCluster(citation, citations_pre, citations_post)
   -- util.debug(string.format('processCitationCluster(%s)', citation.citationID))
-  self:check_valid_citation_element()
-  citation = self:normalize_citation_input(citation)
-  self:_check_input(citation, citationsPre, citationsPost)
+  self:_check_valid_citation_element()
+  citation = self:_normalize_citation_input(citation)
+  self:_check_input(citation, citations_pre, citations_post)
 
-  -- Registor citation
-  self.registry.citations_by_id[citation.citationID] = citation
+  local citation_list, item_ids = self:_build_reconstituted_citation_list(citation, citations_pre, citations_post)
+  self:updateItems(item_ids)
+  if #citation.sorted_items > 1 and self.style.citation.sort then
+    citation.sorted_items = self.style.citation:sorted_citation_items(citation.citationItems, self)
+  end
 
-  local citation_note_pairs = {}
-  util.extend(citation_note_pairs, citationsPre)
-  table.insert(citation_note_pairs, {citation.citationID, citation.properties.noteIndex})
-  util.extend(citation_note_pairs, citationsPost)
-  -- util.debug(citation_note_pairs)
+  local tainted_citation_ids = self:_set_positions(citation_list)
 
-  local citations_by_id = {}
-  local citation_list = {}
-  for _, pair in ipairs(citation_note_pairs) do
-    local citation_id, note_number = table.unpack(pair)
-    local citation_ = self.registry.citations_by_id[citation_id]
-    if not citation_ then
-      util.error("Citation not in registry.")
-    end
-    citations_by_id[citation_.citationID] = citation_
-    table.insert(citation_list, citation_)
-  end
-  self.registry.citations_by_id = citations_by_id
-  self.registry.citation_list = citation_list
+  tainted_citation_ids[citation.citationID] = true
 
-  -- update self.registry.citations_by_item_id
-  local item_ids = {}
-  self.registry.citations_by_item_id = {}
-  for _, citation_ in ipairs(citation_list) do
-    for _, cite_item in ipairs(citation_.citationItems) do
-      if not self.registry.citations_by_item_id[cite_item.id] then
-        self.registry.citations_by_item_id[cite_item.id] = {}
-        table.insert(item_ids, cite_item.id)
-      end
-      table.insert(self.registry.citations_by_item_id[cite_item.id], citation_)
-    end
-  end
-  self:updateItems(item_ids)
-
   local params = {
     bibchange = false,
     citation_errors = {},
   }
-  local output = {}
+  -- TODO: evaluate params.bibchange
 
-  local tainted_citation_ids = self:get_tainted_citation_ids(citation_note_pairs)
-  tainted_citation_ids[citation.citationID] = true
+  local result = self:_rerun_changed_cites(tainted_citation_ids)
 
-  -- Citeproc-js marks all related citations as tainted but I don't think it's
-  -- necessary.
-  if self.style.class == "note" and self.style.has_disambiguate then
-    for _, cite_item in ipairs(citation.citationItems) do
-      for _, citation_ in ipairs(self.registry.citations_by_item_id[cite_item.id]) do
-        tainted_citation_ids[citation_.citationID] = true
-      end
-    end
+  return {params, result}
+end
+
+-- A variant of processCitationCluster() for easy use with LaTeX.
+-- It should be run after refreshing the registry (updateItems()) with all items
+--- at param citation CitationData
+--- at return string
+function CiteProc:process_citation(citation)
+  -- util.debug(string.format('process_citation(%s)', citation.citationID))
+  self:_check_valid_citation_element()
+  citation = self:_normalize_citation_input(citation)
+
+  local citations_pre = {}
+  for _, citation_ in ipairs(self.registry.citation_list) do
+    table.insert(citations_pre, {citation_.citationID, citation_.properties.noteIndex})
   end
+  self:_check_input(citation, citations_pre, {})
 
-  -- util.debug(tainted_citation_ids)
+  local citation_list, item_ids = self:_build_reconstituted_citation_list(citation, citations_pre, {})
+  -- self:updateItems(item_ids)
+  for i, cite_item in ipairs(citation.citationItems) do
+    self:get_item(cite_item.id)
+  end
+  if #citation.sorted_items > 1 and self.style.citation.sort then
+    citation.sorted_items = self.style.citation:sorted_citation_items(citation.citationItems, self)
+  end
 
-  -- params.bibchange = #tainted_citation_ids > 0
-  for citation_id, _ in pairs(tainted_citation_ids) do
-    local citation_ = self.registry.citations_by_id[citation_id]
+  self:_set_positions(citation_list)
+  local tainted_citation_ids = {[citation.citationID] = true}
+  local result = self:_rerun_changed_cites(tainted_citation_ids)
+  return result[1][2]
+end
 
-    local citation_index = citation_.citation_index
+function CiteProc:makeCitationCluster(citation_items)
+  local special_form = nil
+  local items = {}
 
-    local mode = citation_.properties.mode
-    if mode == "suppress-author" and self.style.class == "note" then
-      mode = nil
+  for i, cite_item in ipairs(citation_items) do
+    cite_item = self:_normalize_cite_item(cite_item)
+    local item_data = self:get_item(cite_item.id)
+
+    -- Create a wrapper of the orignal item from registry so that
+    -- it may hold different `locator` or `position` values for cites.
+    local cite_item = setmetatable(cite_item, {__index = item_data})
+
+    if not special_form then
+      for _, form in ipairs({"author-only", "suppress-author", "coposite"}) do
+        if cite_item[form] then
+          special_form = form
+        end
+      end
     end
-    local citation_element = self.style.citation
-    if mode == "author-only" and self.style.intext then
-      citation_element = self.style.intext
-    elseif mode == "full-cite" then
-      citation_element = self.style.full_citation
+
+    -- Set "first-reference-note-number" variable when called from
+    -- processCitationCluster() > updateItems()
+    local citations = self.registry.citations_by_item_id[cite_item.id]
+    if citations and #citations > 0 then
+      cite_item["first-reference-note-number"] = citations[1].properties.noteIndex
     end
 
-    local citation_str = citation_element:build_citation_str(citation_, self)
-    table.insert(output, {citation_index, citation_str, citation_id})
+    cite_item.position_level = Position.First
+    if self.cite_first_note_numbers[cite_item.id] then
+      cite_item.position_level = Position.Subsequent
+    else
+      self.cite_first_note_numbers[cite_item.id] = 0
+    end
+
+    local preceding_cite
+    if i == 1 then
+      local previous_citation = self.registry.previous_citation
+      if previous_citation then
+        if #previous_citation.citationItems == 1 and previous_citation.citationItems[1].id == cite_item.id then
+          preceding_cite = previous_citation.citationItems[1]
+        end
+      end
+    elseif citation_items[i - 1].id == cite_item.id then
+      preceding_cite = citation_items[i - 1]
+    end
+
+    if preceding_cite then
+      cite_item.position_level = self:_get_ibid_position(cite_item, preceding_cite)
+    end
+
+    table.insert(items, cite_item)
   end
 
-  return {params, output}
+  if self.registry.requires_sorting then
+    self:sort_bibliography()
+  end
+
+  self:_check_valid_citation_element()
+  local citation_element = self.style.citation
+  if special_form == "author-only" and self.style.intext then
+    citation_element = self.style.intext
+  end
+
+  local res = citation_element:build_cluster(items, self)
+
+  -- local context = {
+  --   build = {},
+  --   engine=self,
+  -- }
+  -- local res = self.style:render_citation(items, context)
+
+  self.registry.previous_citation = {
+    citationID = "pseudo-citation",
+    citationItems = items,
+    properties = {
+      noteIndex = 0,
+    }
+  }
+  return res
 end
 
-function CiteProc:normalize_citation_input(citation)
+--- at param bibsection any
+--- at return [{[string]: string | number | boolean}, string[]]
+function CiteProc:makeBibliography(bibsection)
+  -- The bibsection works as a filter described in
+  -- <https://citeproc-js.readthedocs.io/en/latest/running.html#selective-output-with-makebibliography>.
+  if not self.style.bibliography then
+    return {{}, {}}
+  end
+
+  local res = {}
+
+  self.registry.widest_label = ""
+  self.registry.maxoffset = 0
+
+  local ids = self:_get_sorted_refs()
+  if bibsection then
+    ids = self:_filter_with_bibsection(ids, bibsection)
+  end
+  for _, id in ipairs(ids) do
+    local str = self.style.bibliography:build_bibliography_str(id, self)
+    table.insert(res, str)
+  end
+
+  local bib_start = self.output_format.markups["bibstart"]
+  local bib_end = self.output_format.markups["bibend"]
+  if type(bib_start) == "function" then
+    bib_start = bib_start(self)
+  end
+  if type(bib_end) == "function" then
+    bib_end = bib_end(self)
+  end
+
+  local params = {
+    hangingindent = self.style.bibliography.hanging_indent,
+    ["second-field-align"] = self.style.bibliography.second_field_align or false,
+    linespacing = self.style.bibliography.line_spacing,
+    entryspacing = self.style.bibliography.entry_spacing,
+    maxoffset = self.registry.maxoffset,
+    widest_label = self.registry.widest_label,
+    bibstart = bib_start,
+    bibend = bib_end,
+    entry_ids = util.clone(self.registry.reflist),
+  }
+
+  return {params, res}
+end
+
+function CiteProc:_check_valid_citation_element()
+  if not self.style.citation then
+    if self.style.info and self.style.info.independent_parent then
+      util.error(string.format("This is a dependent style linked to '%s'.", self.style.info.independent_parent))
+    else
+      util.error('No <citation> in style.')
+    end
+  end
+end
+
+--- at param citation CitationData
+--- at return CitationData
+function CiteProc:_normalize_citation_input(citation)
   citation = util.deep_copy(citation)
 
   if not citation.citationID then
@@ -416,7 +522,7 @@
     citation.citationItems = {}
   end
   for i, cite_item in ipairs(citation.citationItems) do
-    citation.citationItems[i] = self:normalize_cite_item(cite_item)
+    citation.citationItems[i] = self:_normalize_cite_item(cite_item)
   end
 
   -- Fix missing noteIndex: sort_CitationNumberPrimaryAscendingViaMacroCitation.txt
@@ -430,12 +536,14 @@
     citation.properties.chapterIndex = 0
   end
 
+  citation.sorted_items = util.clone(citation.citationItems)
+
   return citation
 end
 
 --- at param cite_item CitationItem
 --- at return CitationItem
-function CiteProc:normalize_cite_item(cite_item)
+function CiteProc:_normalize_cite_item(cite_item)
   -- Shallow copy
   cite_item = util.clone(cite_item)
   cite_item.id = tostring(cite_item.id)
@@ -471,24 +579,42 @@
 end
 
 --- at param citation CitationData
---- at param citationsPre PreCitation[]
---- at param citationsPost PostCitation[]
-function CiteProc:_check_input(citation, citationsPre, citationsPost)
-  --- at type {[CitationId]: boolean}
+--- at param citations_pre PreCitation[]
+--- at param citations_post PostCitation[]
+function CiteProc:_check_input(citation, citations_pre, citations_post)
+  local citation_info_list = {}
+  do
+    for i, pre_citation in ipairs(citations_pre) do
+      local citation_id = pre_citation[1]
+      local note_index = pre_citation[2]
+      local chapter_number = pre_citation[3] or self.registry.citations_by_id[citation_id].properties.chapterIndex or 0
+      local name = string.format("citationsPre[%d]", i)
+      table.insert(citation_info_list, {citation_id, note_index, chapter_number, name})
+    end
+    table.insert(citation_info_list, {citation.citationID, citation.properties.noteIndex, citation.properties.chapterIndex or 0, "citation"})
+    for i, post_citation in ipairs(citations_post) do
+      local citation_id = post_citation[1]
+      local note_index = post_citation[2]
+      local chapter_number = post_citation[3] or self.registry.citations_by_id[citation_id].properties.chapterIndex or 0
+      local name = string.format("citationsPost[%d]", i)
+      table.insert(citation_info_list, {citation_id, note_index, chapter_number, name})
+    end
+  end
+
+  --- at type table<CitationId, boolean>
   local citation_dict = {}
   local last_note_number = 0
   local last_chapter_number = 0
 
-  for i, pre_citation in ipairs(citationsPre) do
-    local citation_id = pre_citation[1]
-    local note_number = pre_citation[2]
-    local chapter_number = pre_citation[3]
+  for _, citation_info in ipairs(citation_info_list) do
+    local citation_id, note_index, chapter_number, name = table.unpack(citation_info)
     if citation_dict[citation_id] then
-      error(string.format("Previously referenced citationID '%s' encountered in citationsPre", citation_id))
+      error(string.format("Previously referenced citationID '%s' encountered at %s", name))
     end
+    citation_dict[citation_id] = true
     if chapter_number and chapter_number > 0 then
       if chapter_number < last_chapter_number then
-        util.warning(string.format("Chapter index sequence is not sane at citationsPre[%d]", i))
+        util.warning(string.format("Chapter index sequence is not sane at %s", name))
       end
       if chapter_number ~= last_chapter_number then
         last_note_number = 0
@@ -495,191 +621,196 @@
       end
       last_chapter_number = chapter_number
     end
-    if note_number and note_number > 0 then
-      if note_number < last_note_number then
-        util.warning(string.format("Note index sequence is not sane at citationsPre[%d]", i))
+    if note_index > 0 then
+      if note_index < last_note_number then
+        util.warning(string.format("Note index sequence is not sane at %s", name))
       end
-      last_note_number = note_number
+      last_note_number = note_index
     end
-    citation_dict[citation_id] = true
   end
 
-  do
-    local citation_id = citation.citationID
-    local note_number = citation.properties.noteIndex
-    local chapter_number = citation.properties.chapterIndex
-    if (citation_dict[citation_id]) then
-      error("Citation with previously referenced citationID " .. citation_id)
+end
+
+--- at param citation CitationData
+--- at param citations_pre PreCitation[]
+--- at param citations_post PostCitation[]
+--- at return CitationData[]
+--- at return CiteId[]
+function CiteProc:_build_reconstituted_citation_list(citation, citations_pre, citations_post)
+  self.registry.citations_by_id[citation.citationID] = citation
+
+  --- at type [CitationId, NoteIndex][]
+  local citation_note_pairs = {}
+  util.extend(citation_note_pairs, citations_pre)
+  table.insert(citation_note_pairs, {citation.citationID, citation.properties.noteIndex})
+  util.extend(citation_note_pairs, citations_post)
+
+  --- at type CiteId[]
+  local item_ids = {}
+  --- at type table<ItemId, boolean>
+  local item_id_dict = {}
+  --- at type CitationData[]
+  local citation_list = {}
+  --- at type table<CitationId, CitationData>
+  local citations_by_id = {}
+  -- TODO: Remove citations_by_item_id
+  --- at type table<ItemId, CitationData[]>
+  local citations_by_item_id = {}
+
+  for citation_index, pair in ipairs(citation_note_pairs) do
+    local citation_id, note_index = table.unpack(pair)
+    local citation_ = self.registry.citations_by_id[citation_id]
+    if not citation_ then
+      util.error("Citation not in registry.")
     end
-    if note_number and note_number > 0 then
-      if note_number < last_note_number then
-        util.warning("Note index sequence is not sane for citation " .. citation_id)
-      end
-      last_note_number = note_number
-    end
-    if chapter_number and chapter_number > 0 then
-      if chapter_number < last_chapter_number then
-        util.warning("Chapter index sequence is not sane for citation " .. citation_id)
-      end
-      last_chapter_number = chapter_number
-    end
-    citation_dict[citation_id] = true
-  end
+    citation_.citation_index = citation_index
+    citation_.properties.noteIndex = note_index
 
-  for i, post_citation in ipairs(citationsPost) do
-    local citation_id = post_citation[1]
-    local note_number = post_citation[2]
-    local chapter_number = post_citation[3]
-    if citation_dict[citation_id] then
-      error(string.format("Previously referenced citationID '%s' encountered in citationsPost", citation_id))
-    end
-    if note_number and note_number > 0 then
-      if note_number < last_note_number then
-        util.warning(string.format("Note index sequence is not sane at citationsPost[%d]", i))
+    table.insert(citation_list, citation_)
+    citations_by_id[citation_.citationID] = citation_
+    for _, cite_item in ipairs(citation_.citationItems) do
+      if not item_id_dict[cite_item.id] then
+        item_id_dict[cite_item.id] = true
+        table.insert(item_ids, cite_item.id)
+        citations_by_item_id[cite_item.id] = {}
       end
-      last_note_number = note_number
+      table.insert(citations_by_item_id[cite_item.id], citation_)
     end
-    if chapter_number and chapter_number > 0 then
-      if chapter_number < last_chapter_number then
-        util.warning(string.format("Chapter index sequence is not sane at citationsPost[%d]", i))
-      end
-      last_chapter_number = chapter_number
-    end
-    citation_dict[citation_id] = true
   end
-
+  self.registry.citation_list = citation_list
+  self.registry.citations_by_id = citations_by_id
+  self.registry.citations_by_item_id = citations_by_item_id
+  return citation_list, item_ids
 end
 
--- A variant of processCitationCluster() for easy use with LaTeX.
--- It should be run after refreshing the registry (updateItems()) with all items
-function CiteProc:process_citation(citation)
-  -- util.debug(citation)
-  -- util.debug(citationsPre)
-  -- Fix missing noteIndex: sort_CitationNumberPrimaryAscendingViaMacroCitation.txt
+--- at param citation_list CitationData[]
+--- at return table<CitationId, boolean>
+function CiteProc:_set_positions(citation_list)
+  --- at type table<CitationId, boolean>
+  local tainted_citation_ids = {}
 
-  citation = self:normalize_citation_input(citation)
-
-  -- Registor citation
-  self.registry.citations_by_id[citation.citationID] = citation
-
-  table.insert(self.registry.citation_list, citation)
-
-  local citation_note_pairs = {}
-  for _, citation_ in ipairs(self.registry.citation_list) do
-    table.insert(citation_note_pairs, {citation_.citationID, citation_.properties.noteIndex})
-  end
-
-  -- update self.registry.citations_by_item_id
-  for _, cite_item in ipairs(citation.citationItems) do
-    if not self.registry.citations_by_item_id[cite_item.id] then
-      self.registry.citations_by_item_id[cite_item.id] = {}
+  --- at type {[integer]: CitationData[]}
+  local chapter_citations = {}
+  for _, citation in ipairs(citation_list) do
+    local chapter_number = citation.properties.chapterIndex
+    if not chapter_citations[chapter_number] then
+      chapter_citations[chapter_number] = {}
     end
-    table.insert(self.registry.citations_by_item_id[cite_item.id], citation)
+    table.insert(chapter_citations[chapter_number], citation)
   end
 
-  -- self:updateItems(item_ids)
-  for i, cite_item in ipairs(citation.citationItems) do
-    self:get_item(cite_item.id)
+  for _, citations in pairs(chapter_citations) do
+    self:_update_chapter_positions(citations, tainted_citation_ids)
   end
 
-  local tainted_citation_ids = self:get_tainted_citation_ids(citation_note_pairs)
-
-  local mode = citation.properties.mode
-  if mode == "suppress-author" and self.style.class == "note" then
-    mode = nil
+  -- Update tainted citation ids because of citation-number's change
+  -- The self.tainted_item_ids were added in the sort_bibliography() procedure.
+  for item_id, _ in pairs(self.tainted_item_ids) do
+    if self.registry.citations_by_item_id[item_id] then
+      for _, citation in ipairs(self.registry.citations_by_item_id[item_id]) do
+        tainted_citation_ids[citation.citationID] = true
+      end
+    end
   end
-  local citation_element = self.style.citation
-  if mode == "author-only" and self.style.intext then
-    citation_element = self.style.intext
-  elseif mode == "full-cite" then
-    citation_element = self.style.full_citation
-  end
 
-  local citation_str = citation_element:build_citation_str(citation, self)
-
-  return citation_str
+  return tainted_citation_ids
 end
 
-
-function CiteProc:get_tainted_citation_ids(citation_note_pairs)
-  local tainted_citation_ids = {}
-
-  self.cite_first_note_numbers = {}
-  self.cite_last_note_numbers = {}
-  self.note_citations_map = {}
-  -- {
-  --   1 = {"citation-1", "citation-2"},
-  --   2 = {"citation-2"},
-  -- }
-
-  -- Citations with noteIndex == 0 are in-text citations and they may also
-  -- have position properties.
+--- at param citation_list CitationData[]
+--- at param tainted_citation_ids table<CitationId, boolean>
+--- at return table<string, boolean>
+function CiteProc:_update_chapter_positions(citation_list, tainted_citation_ids)
+  --- at type CitationData[]
   local in_text_citations = {}
+  --- at type CitationData[]
+  local note_citations = {}
+  for _, citation in ipairs(citation_list) do
+    if citation.properties.noteIndex == 0 then
+      table.insert(in_text_citations, citation)
+    else
+      table.insert(note_citations, citation)
+    end
+  end
 
-  local last_chapter_number = 0
+  for _, citations in ipairs({in_text_citations, note_citations}) do
+    --- at type table<CiteId, NoteIndex>
+    local first_ref = {}
+    --- at type table<CiteId, NoteIndex>
+    local last_ref = {}
+    --- at type table<NoteIndex, CitationId[]>
+    local num_citations_in_note = {}
 
-  local previous_citation
-  for citation_index, pair in ipairs(citation_note_pairs) do
-    local citation_id, note_number = table.unpack(pair)
-    -- util.debug(citation_id)
-    local citation = self.registry.citations_by_id[citation_id]
-    citation.properties.noteIndex = note_number
-    citation.citation_index = citation_index
-
-    local chapter_number = citation.properties.chapterIndex
-    if chapter_number and chapter_number ~= last_chapter_number then
-      self.cite_first_note_numbers = {}
-      self.cite_last_note_numbers = {}
-      self.note_citations_map = {}
-      previous_citation = nil
+    for _, citation in ipairs(citations) do
+      local note_index = citation.properties.noteIndex
+      if not num_citations_in_note[note_index] then
+        num_citations_in_note[note_index] = {}
+      end
+      table.insert(num_citations_in_note[note_index], citation.citationID)
     end
 
+    local previous_citation
+    for _, citation in ipairs(citations) do
+      local mode = citation.properties.mode
+      local note_index = citation.properties.noteIndex
+      local previous_cite
+      for _, cite_item in ipairs(citation.sorted_items) do
+        local position_properties = {
+          position_level = cite_item.position_level,
+          ["first-reference-note-number"] = cite_item["first-reference-note-number"],
+          near_note = cite_item.near_note,
+        }
 
-    local tainted = false
+        self:_set_cite_item_position(cite_item, note_index, previous_cite, previous_citation, citation, first_ref, last_ref, num_citations_in_note)
 
-    local prev_citation = previous_citation
-    if note_number == 0 then
-      -- Find the previous in-text citation.
-      prev_citation = in_text_citations[#in_text_citations]
-    end
-    local previous_cite
-    for _, cite_item in ipairs(citation.citationItems) do
-      tainted = self:set_cite_item_position(cite_item, note_number, previous_cite, prev_citation, citation)
+        if self:_check_tainted_position_change(cite_item, position_properties) then
+          tainted_citation_ids[citation.citationID] = true
+        end
 
-      -- https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#citations
-      -- Citations within the main text of the document have a noteIndex of zero.
-      if (self.style.class == "note" and note_number > 0) or citation.properties.mode ~= "author-only" then
-        self.cite_last_note_numbers[cite_item.id] = note_number
-        previous_cite = cite_item
+        -- https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#citations
+        -- Citations within the main text of the document have a noteIndex of zero.
+        if mode ~= "author-only" and mode ~= "full-cite" then
+          if not first_ref[cite_item.id] and note_index > 0 then
+            -- note_index == 0 implied an in-text citation
+            first_ref[cite_item.id] = note_index
+          end
+          last_ref[cite_item.id] = note_index
+          previous_cite = cite_item
+        end
       end
-    end
 
-    if tainted then
-      tainted_citation_ids[citation.citationID] = true
-    end
-
-    if not self.note_citations_map[note_number] then
-      self.note_citations_map[note_number] = {}
-    end
-    table.insert(self.note_citations_map[note_number], citation.citationID)
-    if citation.properties.mode ~= "author-only" then
-      if note_number == 0 then
-        table.insert(in_text_citations, citation)
-      else
+      if mode ~= "author-only" and mode ~= "full-cite" then
         previous_citation = citation
       end
     end
 
-    last_chapter_number = chapter_number
-  end
-
-  -- Update tainted citation ids because of citation-number's change
-  -- The self.tainted_item_ids were added in the sort_bibliography() procedure.
-  for item_id, _ in pairs(self.tainted_item_ids) do
-    if self.registry.citations_by_item_id[item_id] then
-      for _, citation in ipairs(self.registry.citations_by_item_id[item_id]) do
-        tainted_citation_ids[citation.citationID] = true
+    if self.style.class == "note" and self.style.has_disambiguate then
+      --- at type table<CiteId, CitationData[]>
+      local citations_by_item_id = {}
+      for _, citation in ipairs(citations) do
+        if citation.properties.mode ~= "author-only" and citation.properties.mode ~= "full-cite" then
+          for _, cite_item in ipairs(citation.sorted_items) do
+            if not citations_by_item_id[cite_item.id] then
+              citations_by_item_id[cite_item.id] = {}
+            end
+            table.insert(citations_by_item_id[cite_item.id], citation)
+          end
+        end
       end
+      for _, citation in ipairs(citations) do
+        if citation.properties.mode ~= "author-only" and citation.properties.mode ~= "full-cite" then
+          for _, cite_item in ipairs(citation.sorted_items) do
+            assert(citations_by_item_id[cite_item.id])
+            local num_citations = #citations_by_item_id[cite_item.id]
+            if not cite_item.num_citations or (num_citations < 2) ~= (cite_item.num_citations < 2) then
+              -- self.tainted_item_ids[cite_item.id] = true
+              for _, citation_ in ipairs(citations_by_item_id[cite_item.id]) do
+                tainted_citation_ids[citation_.citationID] = true
+              end
+            end
+            cite_item.num_citations = num_citations
+          end
+        end
+      end
     end
   end
 
@@ -686,58 +817,41 @@
   return tainted_citation_ids
 end
 
-function CiteProc:set_cite_item_position(cite_item, note_number, previous_cite, previous_citation, citation)
-  local position = Position.First
-
+function CiteProc:_set_cite_item_position(cite_item, note_index, previous_cite, previous_citation, citation, first_ref, last_ref, num_citations_in_note)
   -- https://citeproc-js.readthedocs.io/en/latest/csl-json/markup.html#citations
   -- Citations within the main text of the document have a noteIndex of zero.
-  if citation.properties.mode == "author-only" then
+  if citation.properties.mode == "author-only" or citation.properties.mode == "full-cite" then
     -- discretionary_IbidInAuthorDateStyleWithoutIntext.txt
-    cite_item.position_level = position
-    return false
+    cite_item.position_level = Position.First
+    cite_item.near_note = false
+    return
   end
 
-  local first_reference_note_number = self.cite_first_note_numbers[cite_item.id]
+  local first_reference_note_number = first_ref[cite_item.id]
   if first_reference_note_number then
-    position = Position.Subsequent
-  elseif note_number > 0 then
-    -- note_number == 0 implied an in-text citation
-    self.cite_first_note_numbers[cite_item.id] = note_number
+    cite_item.position_level = Position.Subsequent
+    cite_item["first-reference-note-number"] = first_reference_note_number
+  else
+    cite_item.position_level = Position.First
   end
 
-  local preceding_cite_item = self:get_preceding_cite_item(cite_item, previous_cite, previous_citation, note_number)
+  local preceding_cite_item = self:_find_preceding_ibid_item(cite_item, previous_cite, previous_citation, note_index, num_citations_in_note)
 
   if preceding_cite_item then
-    position = self:_get_cite_position(cite_item, preceding_cite_item)
+    cite_item.position_level = self:_get_ibid_position(cite_item, preceding_cite_item)
   end
 
-  local near_note = false
-  local last_note_number = self.cite_last_note_numbers[cite_item.id]
+  cite_item.near_note = false
+  local last_note_number = last_ref[cite_item.id]
   if last_note_number then
-    local note_distance = note_number - last_note_number
-    if note_distance <= self.style.citation.near_note_distance then
-      near_note = true
-    end
+    local note_distance = note_index - last_note_number
+    cite_item.near_note = (note_distance <= self.style.citation.near_note_distance)
   end
 
-  local tainted = false
-  if cite_item.position_level ~= position then
-    tainted = true
-    cite_item.position_level = position
-  end
-  if cite_item["first-reference-note-number"] ~= first_reference_note_number then
-    tainted = true
-    cite_item["first-reference-note-number"] = first_reference_note_number
-  end
-  if cite_item.near_note ~= near_note then
-    tainted = true
-    cite_item.near_note = near_note
-  end
-  return tainted
 end
 
 -- Find the preceding cite referencing the same item
-function CiteProc:get_preceding_cite_item(cite_item, previous_cite, previous_citation, note_number)
+function CiteProc:_find_preceding_ibid_item(cite_item, previous_cite, previous_citation, note_index, num_citations_in_note)
   if previous_cite then
     -- a. the current cite immediately follows on another cite, within the same
     --    citation, that references the same item
@@ -752,11 +866,11 @@
     -- b. the current cite is the first cite in the citation, and the previous
     --    citation consists of a single cite referencing the same item
     local previous_note_number = previous_citation.properties.noteIndex
-    local num_previous_note_citations = #self.note_citations_map[previous_note_number]
-    if (previous_note_number == note_number - 1 and num_previous_note_citations == 1)
-        or previous_note_number == note_number then
-      if #previous_citation.citationItems == 1 then
-        previous_cite = previous_citation.citationItems[1]
+    local num_previous_note_citations = #num_citations_in_note[previous_note_number]
+    if (previous_note_number == note_index - 1 and num_previous_note_citations == 1)
+        or previous_note_number == note_index then
+      if #previous_citation.sorted_items == 1 then
+        previous_cite = previous_citation.sorted_items[1]
         if previous_cite.id == cite_item.id then
           return previous_cite
         end
@@ -766,7 +880,7 @@
   return nil
 end
 
-function CiteProc:_get_cite_position(item, preceding_cite)
+function CiteProc:_get_ibid_position(item, preceding_cite)
   if preceding_cite.locator then
     if item.locator then
       if item.locator == preceding_cite.locator and item.label == preceding_cite.label then
@@ -786,135 +900,44 @@
   end
 end
 
-function CiteProc:makeCitationCluster(citation_items)
-  local special_form = nil
-  local items = {}
-
-  for i, cite_item in ipairs(citation_items) do
-    cite_item = self:normalize_cite_item(cite_item)
-    local item_data = self:get_item(cite_item.id)
-
-    -- Create a wrapper of the orignal item from registry so that
-    -- it may hold different `locator` or `position` values for cites.
-    local cite_item = setmetatable(cite_item, {__index = item_data})
-
-    if not special_form then
-      for _, form in ipairs({"author-only", "suppress-author", "coposite"}) do
-        if cite_item[form] then
-          special_form = form
-        end
-      end
+function CiteProc:_check_tainted_position_change(cite_item, position_properties)
+  for key, value in pairs(position_properties) do
+    if cite_item[key] ~= value then
+      return true
     end
+  end
+  return false
+end
 
-    -- Set "first-reference-note-number" variable when called from
-    -- processCitationCluster() > updateItems()
-    local citations = self.registry.citations_by_item_id[cite_item.id]
-    if citations and #citations > 0 then
-      item_data["first-reference-note-number"] = citations[1].properties.noteIndex
+--- at param tainted_citation_ids table<CitationId, boolean>
+--- at return [integer, string, CitationId][]
+function CiteProc:_rerun_changed_cites(tainted_citation_ids)
+  local result = {}
+  for citation_id, _ in pairs(tainted_citation_ids) do
+    local citation = self.registry.citations_by_id[citation_id]
+    local citation_index = citation.citation_index
+    local mode = citation.properties.mode
+    if mode == "suppress-author" and self.style.class == "note" then
+      mode = nil
     end
-
-    cite_item.position_level = Position.First
-    if self.cite_first_note_numbers[cite_item.id] then
-      cite_item.position_level = Position.Subsequent
-    else
-      self.cite_first_note_numbers[cite_item.id] = 0
-    end
-
-    local preceding_cite
-    if i == 1 then
-      local previous_citation = self.registry.previous_citation
-      if previous_citation then
-        if #previous_citation.citationItems == 1 and previous_citation.citationItems[1].id == cite_item.id then
-          preceding_cite = previous_citation.citationItems[1]
-        end
+    local citation_element = self.style.citation
+    if mode == "author-only" and self.style.intext then
+      citation_element = self.style.intext
+    elseif mode == "full-cite" then
+      if self.style.class == "note" then
+        citation_element = self.style.citation
+      else
+        citation_element = self.style.full_citation
       end
-    elseif citation_items[i - 1].id == cite_item.id then
-      preceding_cite = citation_items[i - 1]
     end
 
-    if preceding_cite then
-      cite_item.position_level = self:_get_cite_position(cite_item, preceding_cite)
-    end
-
-    table.insert(items, cite_item)
+    local citation_str = citation_element:build_citation_str(citation, self)
+    table.insert(result, {citation_index, citation_str, citation_id})
   end
-
-  if self.registry.requires_sorting then
-    self:sort_bibliography()
-  end
-
-  self:check_valid_citation_element()
-  local citation_element = self.style.citation
-  if special_form == "author-only" and self.style.intext then
-    citation_element = self.style.intext
-  end
-
-  local res = citation_element:build_cluster(items, self)
-
-  -- local context = {
-  --   build = {},
-  --   engine=self,
-  -- }
-  -- local res = self.style:render_citation(items, context)
-
-  self.registry.previous_citation = {
-    citationID = "pseudo-citation",
-    citationItems = items,
-    properties = {
-      noteIndex = 0,
-    }
-  }
-  return res
+  return result
 end
 
---- at param bibsection any
---- at return [{[string]: string | number | boolean}, string[]]
-function CiteProc:makeBibliography(bibsection)
-  -- The bibsection works as a filter described in
-  -- <https://citeproc-js.readthedocs.io/en/latest/running.html#selective-output-with-makebibliography>.
-  if not self.style.bibliography then
-    return {{}, {}}
-  end
-
-  local res = {}
-
-  self.registry.widest_label = ""
-  self.registry.maxoffset = 0
-
-  local ids = self:get_sorted_refs()
-  if bibsection then
-    ids = self:filter_with_bibsection(ids, bibsection)
-  end
-  for _, id in ipairs(ids) do
-    local str = self.style.bibliography:build_bibliography_str(id, self)
-    table.insert(res, str)
-  end
-
-  local bib_start = self.output_format.markups["bibstart"]
-  local bib_end = self.output_format.markups["bibend"]
-  if type(bib_start) == "function" then
-    bib_start = bib_start(self)
-  end
-  if type(bib_end) == "function" then
-    bib_end = bib_end(self)
-  end
-
-  local params = {
-    hangingindent = self.style.bibliography.hanging_indent,
-    ["second-field-align"] = self.style.bibliography.second_field_align or false,
-    linespacing = self.style.bibliography.line_spacing,
-    entryspacing = self.style.bibliography.entry_spacing,
-    maxoffset = self.registry.maxoffset,
-    widest_label = self.registry.widest_label,
-    bibstart = bib_start,
-    bibend = bib_end,
-    entry_ids = util.clone(self.registry.reflist),
-  }
-
-  return {params, res}
-end
-
-function CiteProc:get_sorted_refs()
+function CiteProc:_get_sorted_refs()
   if self.registry.requires_sorting then
     self:sort_bibliography()
   end
@@ -921,7 +944,7 @@
   return self.registry.reflist
 end
 
-function CiteProc:filter_with_bibsection(ids, bibsection)
+function CiteProc:_filter_with_bibsection(ids, bibsection)
   if bibsection.quash then
     return self:filter_quash(ids, bibsection)
   elseif bibsection.select then

Modified: trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-manager.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-manager.lua	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-manager.lua	2024-09-15 19:57:10 UTC (rev 72292)
@@ -7,6 +7,7 @@
 local citeproc_manager = {}
 
 require("lualibs")
+local json_encode = utilities.json.tojson
 local json_decode = utilities.json.tolua
 
 local citeproc = require("citeproc")
@@ -14,7 +15,7 @@
 local bibtex_parser = require("citeproc-bibtex-parser")
 local latex_parser = require("citeproc-latex-parser")
 local util = citeproc.util
-local unicode =  require("citeproc-unicode")
+local unicode = require("citeproc-unicode")
 
 
 --- at param file_name string
@@ -221,6 +222,7 @@
 --- at alias LanguageCode string
 
 --- at class RefSection
+--- at field index integer
 --- at field initialized boolean
 --- at field style_id string?
 --- at field bib_resources FilePath[]
@@ -233,6 +235,7 @@
 --- at field citations CitationData[]
 --- at field citations_pre table[]
 --- at field bibliography_configs string[]
+--- at field style string
 local RefSection = {}
 
 --- at return RefSection
@@ -239,6 +242,7 @@
 function RefSection:new()
   --- at type RefSection
   local ref_section = {
+    index = 0,
     initialized = false,
     style_id = nil,
     bib_resources = {},
@@ -251,6 +255,7 @@
     citations = {},
     citations_pre = {},
     bibliography_configs = {},
+    style = "",
   }
   setmetatable(ref_section, self)
   self.__index = self
@@ -276,6 +281,9 @@
     self.lang = nil
   end
 
+  self.style = style
+  -- self.items
+
   self.engine = citeproc.new(citeproc_sys, style, self.lang, force_lang)
   self:_check_dependent_style(citeproc_sys)
 
@@ -334,7 +342,87 @@
   self.uncited_ids = uncited_ids
 end
 
+function RefSection:record_fixture()
+  if not self.fixture then
+    self.fixture = {
+      mode = "citation",
+      description = tex.jobname,
+      result = "",
+      bibliography_result = "",
+      citations = "",
+      ["citation-items"] = "",
+      csl = self.style,
+      input = "",
+      version = "1.0",
+      citation_results = {},
+    }
+  end
+  local fixture = self.fixture
 
+  local cited_id_dict = {}
+  for _, citation in ipairs(self.citations) do
+    for _, cite_item in ipairs(citation.citationItems) do
+      cited_id_dict[cite_item.id] = true
+    end
+  end
+  local input_items = {}
+  for _, item in ipairs(self.items) do
+    if cited_id_dict[item.id] then
+      table.insert(input_items, item)
+    end
+  end
+  fixture.input = json_encode(input_items)
+
+  local citation_items = {}
+  for _, citation in ipairs(self.citations) do
+    table.insert(citation_items, util.clone(citation.citationItems))
+  end
+  fixture["citation-items"] = json_encode(citation_items)
+
+  local citations = {}
+  local citations_pre = {}
+  for _, citation in ipairs(self.citations) do
+    table.insert(citations, {citation, util.clone(citations_pre), {}})
+    table.insert(citations_pre, {citation.citationID, citation.properties.noteIndex})
+  end
+  fixture.citations = string.gsub(json_encode(citations), "{%s*}", "[]")
+
+  local result = {}
+  for i, citation_result in ipairs(fixture.citation_results) do
+    if i == #fixture.citation_results then
+      table.insert(result, string.format(">>[%d] %s", i - 1, citation_result))
+    else
+      table.insert(result, string.format("..[%d] %s", i - 1, citation_result))
+    end
+  end
+  fixture.result = table.concat(result, "\n")
+
+  local fixture_output = {}
+  local function make_block(section, str)
+    return string.format(">>===== %s =====>>\n%s\n<<===== %s =====<<\n", string.upper(section), str, string.upper(section))
+  end
+  for _, section in ipairs({"mode", "description", "result", "bibliography_result", "citations", "citation-items", "csl", "input", "version"}) do
+    if not (section == "bibliography_result" and fixture[section] == "") then
+      table.insert(fixture_output, make_block(section, fixture[section]))
+    end
+  end
+  local fixture_str = table.concat(fixture_output, "\n\n")
+
+  local file_name = string.format("%s.txt", tex.jobname)
+  if self.index > 0 then
+    file_name = string.format("%s-%d.txt", tex.jobname, self.index)
+  end
+
+  local fixture_file = io.open(file_name, "w")
+  if fixture_file then
+    fixture_file:write(fixture_str)
+    fixture_file:close()
+  else
+    error(string.format("Failed to write fixture file '%s'", file_name))
+  end
+end
+
+
 --- at class CslCitationManager
 --- at field global_ref_section RefSection
 --- at field max_ref_section_index integer
@@ -342,6 +430,7 @@
 --- at field ref_sections table<integer, RefSection>
 --- at field ref_section RefSection
 --- at field hyperref_loaded boolean
+--- at field regression_test boolean
 local CslCitationManager = {}
 
 --- at return CslCitationManager
@@ -361,6 +450,7 @@
     ref_section = ref_section,
 
     hyperref_loaded = false,
+    regression_test = false,
   }
 
   setmetatable(o, self)
@@ -388,6 +478,10 @@
   if self.hyperref_loaded then
     global_ref_section.engine:enable_linking()
   end
+  if self.regression_test then
+    self.global_ref_section:record_fixture()
+  end
+
   self.ref_section_index = 0
 end
 
@@ -414,6 +508,7 @@
   self.ref_section = self.ref_sections[self.ref_section_index]
   if not self.ref_section then
     self.ref_section = RefSection:new()
+    self.ref_section.index = self.ref_section_index
     self.ref_sections[self.ref_section_index] = self.ref_section
   end
 
@@ -434,6 +529,10 @@
     self.ref_section.engine:enable_linking()
   end
 
+  if self.regression_test then
+    self.ref_section:record_fixture()
+  end
+
 end
 
 
@@ -456,6 +555,7 @@
   if not ref_section then
     ref_section = RefSection:new()
     self.ref_sections[ref_section_index] = ref_section
+    ref_section.index = ref_section_index
   end
 
   local citation = self:_make_citation(citation_info)
@@ -493,6 +593,8 @@
   local citation = self:_make_citation(citation_info)
   -- util.debug(citation)
 
+  table.insert(self.ref_section.citations, citation)
+
   local citation_str
   -- if preview_mode then
   --   -- TODO: preview mode in first pass of \blockquote of csquotes
@@ -512,6 +614,11 @@
   -- and <https://tex.stackexchange.com/questions/519954/backslashes-in-macros-defined-in-lua-code>.
   tex.sprint(string.format("\\expandafter\\def\\csname l__csl_citation_tl\\endcsname{%s}", citation_str))
 
+  if self.regression_test then
+    table.insert(self.ref_section.fixture.citation_results, citation_str)
+    self.ref_section:record_fixture()
+  end
+
   table.insert(self.ref_section.citations_pre, {citation.citationID, citation.properties.noteIndex})
 end
 
@@ -627,6 +734,11 @@
   end
   local bib_lines = self:make_bibliography(filter_str)
   tex.print(util.split(table.concat(bib_lines, "\n"), "\n"))
+
+  if self.regression_test then
+    self.ref_section.fixture.bibliography_result = table.concat(bib_lines, "\n")
+    self.ref_section:record_fixture()
+  end
 end
 
 --- at param filter_str string
@@ -799,6 +911,7 @@
     if not ref_section then
       ref_section = RefSection:new()
       self.ref_sections[ref_section_index] = ref_section
+      ref_section.index = ref_section_index
       self.ref_section = ref_section
     end
     if command == "\\csl at aux@style" then

Modified: trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-node-citation.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-node-citation.lua	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-node-citation.lua	2024-09-15 19:57:10 UTC (rev 72292)
@@ -178,13 +178,6 @@
 
 --- at alias CiteId string | number
 
---- at class CitationItem
---- at field id CiteId
---- at field prefix string?
---- at field suffix string?
---- at field locator string?
---- at field label string?
-
 ---comment
 --- at param citation_items CitationItem[]
 --- at param engine CiteProc
@@ -195,6 +188,8 @@
   local output_format = engine.output_format
   --- at type CiteIr[]
   local irs = {}
+  -- The citation_items are already sorted when evaluate the positions
+  -- To be removed
   citation_items = self:sorted_citation_items(citation_items, engine)
   for _, cite_item in ipairs(citation_items) do
     local ir = self:build_fully_disambiguated_ir(cite_item, output_format, engine, properties)
@@ -437,7 +432,7 @@
 --- at param cite_item CitationItem
 --- at param output_format OutputFormat
 --- at param engine CiteProc
---- at param properties table
+--- at param properties CitationProperties
 --- at return CiteIr
 function Citation:build_fully_disambiguated_ir(cite_item, output_format, engine, properties)
   -- util.debug(cite_item.id)
@@ -451,6 +446,7 @@
     -- Disambiguation should be based on the subsequent form
     -- disambiguate_BasedOnEtAlSubsequent.txt
     cite_item.position_level = Position.Subsequent
+    cite_item["first-reference-note-number"] = properties.noteIndex
     local disam_ir = self:build_ambiguous_ir(cite_item, output_format, engine)
     disam_ir = self:apply_disambiguate_add_givenname(disam_ir, engine)
     disam_ir = self:apply_disambiguate_add_names(disam_ir, engine)
@@ -457,6 +453,7 @@
     disam_ir = self:apply_disambiguate_conditionals(disam_ir, engine)
     disam_ir = self:apply_disambiguate_add_year_suffix(disam_ir, engine)
     cite_item.position_level = Position.First
+    cite_item["first-reference-note-number"] = nil
   end
 
   return cite_ir
@@ -1034,7 +1031,6 @@
         -- Update ir output
         local inlines = ir_:flatten(disam_format)
         local disam_str = disam_format:output(inlines, nil)
-        -- util.debug("update: " .. ir_.cite_item.id .. ": " .. disam_str)
         ir_.disam_str = disam_str
         if not engine.cite_irs_by_output[disam_str] then
           engine.cite_irs_by_output[disam_str] = {}

Modified: trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/scripts/citation-style-language/citeproc-util.lua	2024-09-15 19:57:10 UTC (rev 72292)
@@ -1063,7 +1063,7 @@
 
 -- Choose
 
---- at enum Postion
+--- at enum Position
 local Position = {
   First = 0,
   Subsequent = 1,

Modified: trunk/Master/texmf-dist/scripts/citation-style-language/citeproc.lua
===================================================================
--- trunk/Master/texmf-dist/scripts/citation-style-language/citeproc.lua	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/scripts/citation-style-language/citeproc.lua	2024-09-15 19:57:10 UTC (rev 72292)
@@ -16,7 +16,7 @@
   util = require("citeproc.util")
 end
 
-citeproc.__VERSION__ = "0.6.3"
+citeproc.__VERSION__ = "0.6.4"
 
 citeproc.new = engine.CiteProc.new
 citeproc.util = util

Modified: trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language-init.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language-init.sty	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language-init.sty	2024-09-15 19:57:10 UTC (rev 72292)
@@ -83,6 +83,10 @@
 
 \cs_new:Npn \__csl_initialize_lua_module:
   {
+    \bool_if:NT \l__csl_regression_test_bool
+      {
+        \lua_now:n { csl_citation_manager.regression_test = true }
+      }
     \tl_set:Nx \l_tmpa_tl { \clist_use:Nn \l__csl_bib_resources_clist { , } }
     \lua_now:e
       {

Modified: trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language.sty
===================================================================
--- trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language.sty	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/tex/latex/citation-style-language/citation-style-language.sty	2024-09-15 19:57:10 UTC (rev 72292)
@@ -9,7 +9,7 @@
 \RequirePackage{expl3}
 \RequirePackage{xparse}
 
-\ProvidesExplPackage {citation-style-language} {2024-08-28} {0.6.3}
+\ProvidesExplPackage {citation-style-language} {2024-09-15} {0.6.4}
   {Citation Style Language for LaTeX}
 
 \RequirePackage { l3keys2e }

Modified: trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-AT.xml
===================================================================
--- trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-AT.xml	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-AT.xml	2024-09-15 19:57:10 UTC (rev 72292)
@@ -20,7 +20,7 @@
       <name>Frank Bennett</name>
     </translator>
     <rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
-    <updated>2024-03-09T15:20:54-05:00</updated>
+    <updated>2024-08-27T14:30:55-04:00</updated>
   </info>
   <style-options punctuation-in-quote="false"/>
   <date form="text">
@@ -46,7 +46,7 @@
     <term name="no-publisher" form="short">n.p.</term>
     <term name="on">on</term>
     <term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form  -->
-    <term name="original-work-published">original work published</term>
+    <term name="original-work-published">Ursprünglich erschienen</term>
     <term name="personal-communication">persönliche Kommunikation</term>
     <term name="podcast">podcast</term>
     <term name="podcast-episode">podcast episode</term>

Modified: trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-CH.xml
===================================================================
--- trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-CH.xml	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-CH.xml	2024-09-15 19:57:10 UTC (rev 72292)
@@ -14,7 +14,7 @@
       <name>Sebastian Karcher</name>
     </translator>
     <rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
-    <updated>2024-03-09T15:20:54-05:00</updated>
+    <updated>2024-08-27T14:30:55-04:00</updated>
   </info>
   <style-options punctuation-in-quote="false"/>
   <date form="text">
@@ -40,7 +40,7 @@
     <term name="no-publisher" form="short">n.p.</term>
     <term name="on">on</term>
     <term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form  -->
-    <term name="original-work-published">original work published</term>
+    <term name="original-work-published">Ursprünglich erschienen</term>
     <term name="personal-communication">persönliche Kommunikation</term>
     <term name="podcast">podcast</term>
     <term name="podcast-episode">podcast episode</term>

Modified: trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-DE.xml
===================================================================
--- trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-DE.xml	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/tex/latex/citation-style-language/locales/csl-locales-de-DE.xml	2024-09-15 19:57:10 UTC (rev 72292)
@@ -17,7 +17,7 @@
       <name>jakov</name>
     </translator>
     <rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
-    <updated>2024-03-09T15:20:54-05:00</updated>
+    <updated>2024-08-27T14:30:55-04:00</updated>
   </info>
   <style-options punctuation-in-quote="false"/>
   <date form="text">
@@ -43,7 +43,7 @@
     <term name="no-publisher" form="short">n.p.</term>
     <term name="on">on</term>
     <term name="op-cit">op. cit.</term> <!-- like ibid., the abbreviated form is the regular form  -->
-    <term name="original-work-published">original work published</term>
+    <term name="original-work-published">Ursprünglich erschienen</term>
     <term name="personal-communication">persönliche Kommunikation</term>
     <term name="podcast">podcast</term>
     <term name="podcast-episode">podcast episode</term>

Modified: trunk/Master/texmf-dist/tex/latex/citation-style-language/styles/apa.csl
===================================================================
--- trunk/Master/texmf-dist/tex/latex/citation-style-language/styles/apa.csl	2024-09-15 19:56:50 UTC (rev 72291)
+++ trunk/Master/texmf-dist/tex/latex/citation-style-language/styles/apa.csl	2024-09-15 19:57:10 UTC (rev 72292)
@@ -14,7 +14,7 @@
     <category citation-format="author-date"/>
     <category field="psychology"/>
     <category field="generic-base"/>
-    <updated>2024-08-24T12:16:51-04:00</updated>
+    <updated>2024-08-27T14:44:31-04:00</updated>
     <rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
   </info>
   <locale xml:lang="en">
@@ -832,7 +832,7 @@
                 <group delimiter=" ">
                   <text variable="genre" text-case="capitalize-first"/>
                   <group delimiter=" ">
-                    <text term="author" form="verb"/>
+                    <text term="container-author" form="verb"/>
                     <names variable="interviewer">
                       <name and="symbol" initialize-with=". " delimiter=", "/>
                     </names>



More information about the tex-live-commits mailing list.