publ-aut.lua /size: 33 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['publ-aut'] = {
2    version   = 1.001,
3    comment   = "this module part of publication support",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9if not characters then
10    dofile(resolvers.findfile("char-def.lua"))
11    dofile(resolvers.findfile("char-ini.lua"))
12end
13
14local lpeg = lpeg
15
16local type, next, tostring, tonumber = type, next, tostring, tonumber
17local concat, sortedhash = table.concat, table.sortedhash
18local utfsub = utf.sub
19local find = string.find
20local formatters = string.formatters
21
22local P, S, C, V, Cs, Ct, Cg, Cf, Cc = lpeg.P, lpeg.S, lpeg.C, lpeg.V, lpeg.Cs, lpeg.Ct, lpeg.Cg, lpeg.Cf, lpeg.Cc
23local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
24local settings_to_hash = utilities.parsers.settings_to_hash
25
26local context         = context
27----- commands        = commands
28
29local implement       = interfaces.implement
30
31local publications    = publications
32
33local datasets        = publications.datasets
34local getcasted       = publications.getcasted
35
36local allocate        = utilities.storage.allocate
37
38local chardata        = characters.data
39
40local trace_hashing   = false  trackers.register("publications.authorhash", function(v) trace_hashing = v end)
41
42local expand_authors  = false  directives.register("publications.prerollauthor", function(v) expand_authors = v end)
43
44local report          = logs.reporter("publications","authors")
45local report_cite     = logs.reporter("publications","cite")
46
47local v_last          = interfaces.variables.last
48
49-- local function makesplitter(separator)
50--     return Ct { "start",
51--         start = (Cs((V("outer") + (1-separator))^1) + separator^1)^1,
52--         start = Cs(V("outer")) + (Cs((V("inner") + (1-separator))^1) + separator^1)^1,
53--         outer = (P("{")/"") * ((V("inner") + P(1-P("}")))^0) * (P("}")/""),
54--         inner = P("{") * ((V("inner") + P(1-P("}")))^0) * P("}"),
55--     }
56-- end
57
58-- authorlist = { authorspec and authorspec and authorspec }
59-- authorspec = composedname
60-- authorspec = surnames, firstnames
61-- authorspec = von, surnames, firstnames
62-- authorspec = von, surnames, jr, firstnames
63-- authorspec = von, surnames, jr, firstnames, initials
64
65local space          = lpegpatterns.whitespace
66local comma          = P(",")
67local period         = P(".") + P("{.}")
68local dash           = P("-") + P("{-}")
69local firstcharacter = lpegpatterns.utf8byte
70local utf8character  = lpegpatterns.utf8character
71local p_and          = space^1 * (P("and") + P("&&") + P("++")) * space^1
72local p_comma        = space^0 * comma * space^0
73local p_space        = space^1
74local p_shortone     = C((utf8character      -dash-period)^1)
75local p_longone      = C( utf8character) * (1-dash-period)^0
76
77local p_empty        = P("{}")/"" * #(p_space^0 * (P(-1) + P(",")))
78
79local andsplitter   = Ct { "start",
80    start = (Cs((V("inner") + (1-p_and))^1) + p_and)^1,
81    inner = P("{") * ((V("inner") + P(1-P("}")))^1) * P("}"),
82}
83
84local commasplitter = Ct { "start",
85    start = Cs(V("outer")) + (p_empty + Cs((V("inner") + (1-p_comma))^1) + p_comma)^1,
86    outer = (P("{")/"") * ((V("inner") + P(1-P("}")))^1) * ((P("}") * P(-1))/""),
87    inner = P("{") * ((V("inner") + P(1-P("}")))^1) * P("}"),
88}
89
90local spacesplitter = Ct { "start",
91    start = Cs(V("outer")) + (Cs((V("inner") + (1-p_space))^1) + p_space)^1,
92    outer = (P("{")/"") * ((V("inner") + P(1-P("}")))^1) * ((P("}") * P(-1))/""),
93    inner = P("{") * ((V("inner") + P(1-P("}")))^1) * P("}"),
94}
95
96local p_initial       = p_shortone * period * dash^0
97                      + p_longone * (period + dash + P(-1))
98local initialsplitter = p_initial * P(-1) + Ct((p_initial)^1)
99
100local optionsplitter  = Cf(Ct("") * Cg(C((1-space)^1) * space^0 * Cc(true))^1,rawset)
101
102local function is_upper(str)
103    local first = lpegmatch(firstcharacter,str)
104    local okay = chardata[first]
105    return okay and okay.category == "lu"
106end
107
108-- local cleaner = Cs( ( P("{}")/"" + P(1) )^1 )
109
110local cache   = allocate() -- 33% reuse on tugboat.bib
111local nofhits = 0
112local nofused = 0
113
114publications.authorcache = cache
115
116local function makeinitials(firstnames)
117    if firstnames and #firstnames > 0 then
118        local initials = { }
119        for i=1,#firstnames do
120            initials[i] = lpegmatch(initialsplitter,firstnames[i])
121        end
122        return initials
123    end
124end
125
126local authormap        = allocate()
127publications.authormap = authormap
128
129local prerollcmdstring = publications.prerollcmdstring
130
131local function splitauthor(author,justsplit)
132    local detail, remapped
133    if not justsplit then
134        detail = cache[author]
135        if detail then
136            return detail
137        end
138        remapped = authormap[author]
139        if remapped then
140            report("remapping %a to %a",author,remapped)
141            local detail = cache[remapped]
142            if detail then
143                cache[author] = detail
144                return detail
145            end
146        end
147    end
148    local author = remapped or author
149    local firstnames, vons, surnames, initials, juniors, options
150    if expand_authors and find(author,"\\btxcmd") then
151        author = prerollcmdstring(author)
152    end
153    local split = lpegmatch(commasplitter,author)
154    local n = #split
155    detail = {
156        original = author,
157        snippets = n,
158    }
159    if n == 1 then
160        -- {First Middle von Last}
161        local words = lpegmatch(spacesplitter,author)
162        local i     = 1
163        local n     = #words
164        firstnames  = { }
165        vons        = { }
166        surnames    = { }
167        while i <= n do
168            local w = words[i]
169            if is_upper(w) then
170                firstnames[#firstnames+1], i = w, i + 1
171            else
172                break
173            end
174        end
175        while i <= n do
176            local w = words[i]
177            if is_upper(w) then
178                break
179            else
180                vons[#vons+1], i = w, i + 1
181            end
182        end
183        if i <= n then
184            while i <= n do
185                surnames[#surnames+1], i = words[i], i + 1
186            end
187        elseif #vons == 0 then
188            surnames[1] = firstnames[#firstnames]
189            firstnames[#firstnames] = nil
190        else
191            -- mess
192        end
193        if #surnames == 0 then
194            -- safeguard
195            firstnames = { }
196            vons       = { }
197            surnames   = { author }
198        else
199            initials = makeinitials(firstnames)
200        end
201    elseif n == 2 then
202        -- {Last, First}
203        -- {von Last, First}
204        local words = lpegmatch(spacesplitter,split[1])
205        local i     = 1
206        local n     = #words
207        firstnames = { }
208        vons       = { }
209        surnames   = { }
210        while i <= n do
211            local w = words[i]
212            if is_upper(w) then
213                break
214            else
215                vons[#vons+1], i = w, i + 1
216            end
217        end
218        while i <= n do
219            surnames[#surnames+1] = words[i]
220            i = i + 1
221        end
222        --
223        local words = lpegmatch(spacesplitter,split[2])
224        local i     = 1
225        local n     = #words
226        while i <= n do
227            local w = words[i]
228            if is_upper(w) then
229                firstnames[#firstnames+1] = w
230                i = i + 1
231            else
232                break
233            end
234        end
235        while i <= n do
236            vons[#vons+1] = words[i]
237            i = i + 1
238        end
239        if surnames and firstnames and #surnames == 0 then
240            -- safeguard
241            surnames[1] = firstnames[#firstnames]
242            firstnames[#firstnames] = nil
243        end
244        initials = makeinitials(firstnames)
245    elseif n == 3 then
246        -- {von Last, First, Jr}
247        surnames   = lpegmatch(spacesplitter,split[1])
248        juniors    = lpegmatch(spacesplitter,split[2])
249        firstnames = lpegmatch(spacesplitter,split[3])
250        initials   = makeinitials(firstnames)
251    elseif n == 4 then
252        -- {Von, Last, First, Jr}
253        vons       = lpegmatch(spacesplitter,split[1])
254        surnames   = lpegmatch(spacesplitter,split[2])
255        juniors    = lpegmatch(spacesplitter,split[3])
256        firstnames = lpegmatch(spacesplitter,split[4])
257        initials   = makeinitials(firstnames)
258    elseif n >= 5 then
259        -- {Von, Last, First, Jr, F.}
260        -- {Von, Last, First, Jr, Fr., options}
261        vons       = lpegmatch(spacesplitter,split[1])
262        surnames   = lpegmatch(spacesplitter,split[2])
263        juniors    = lpegmatch(spacesplitter,split[3])
264        firstnames = lpegmatch(spacesplitter,split[4])
265        initials   = lpegmatch(spacesplitter,split[5])
266        options    = split[6]
267        if options then
268            options = lpegmatch(optionsplitter,options)
269        end
270    end
271    if firstnames and #firstnames > 0 then detail.firstnames = firstnames end
272    if vons       and #vons       > 0 then detail.vons       = vons       end
273    if surnames   and #surnames   > 0 then detail.surnames   = surnames   end
274    if initials   and #initials   > 0 then detail.initials   = initials   end
275    if juniors    and #juniors    > 0 then detail.juniors    = juniors    end
276    if options    and next(options)   then detail.options    = options    end
277    if not justsplit then
278        cache[author] = detail
279        nofhits = nofhits + 1
280    end
281    return detail
282end
283
284local function splitauthorstring(str)
285    if not str or str == "" then
286        return
287    end
288    nofused = nofused + 1
289
290    local remapped = authormap[str]
291    if remapped then
292        local detail = cache[remapped]
293        if detail then
294            cache[str] = detail
295            return { detail }
296        end
297    end
298
299    local authors = cache[str]
300    if authors then
301        return { authors } -- we assume one author
302    end
303
304    -- we could cache these too but it can become messy .. leave that for later
305
306    local authors    = lpegmatch(andsplitter,str) or { } -- maybe fake an author
307    local nofauthors = #authors
308    for i=1,nofauthors do
309        authors[i] = splitauthor(authors[i])
310    end
311    if nofauthors > 1 and authors[nofauthors].original == "others" then
312        -- only the last one is looked at
313        authors[nofauthors] = nil
314        authors.others      = true
315    end
316    return authors
317end
318
319publications.splitoneauthor = splitauthor
320publications.splitauthor    = splitauthorstring
321
322local function the_initials(initials,symbol,connector)
323    if not symbol then
324        symbol = "."
325    end
326    if not connector then
327        connector = "-"
328    end
329    local result = { }
330    local r      = 0
331    for i=1,#initials do
332        local initial = initials[i]
333        if type(initial) == "table" then
334            -- J.-J.
335            local set = { }
336            local s   = 0
337            for i=1,#initial do
338                if i > 1 then
339                    s = s + 1 ; set[s] = connector
340                end
341                s = s + 1 ; set[s] = initial[i]
342                s = s + 1 ; set[s] = symbol
343            end
344            r = r + 1 ; result[r] = concat(set)
345        else
346            -- J.
347            r = r + 1 ; result[r] = initial .. symbol
348        end
349    end
350    return result
351end
352
353local ctx_btxsetconcat        = context.btxsetconcat
354local ctx_btxsetoverflow      = context.btxsetoverflow
355local ctx_btxsetinitials      = context.btxsetinitials
356local ctx_btxsetfirstnames    = context.btxsetfirstnames
357local ctx_btxsetvons          = context.btxsetvons
358local ctx_btxsetsurnames      = context.btxsetsurnames
359local ctx_btxsetjuniors       = context.btxsetjuniors
360local ctx_btxsetauthorvariant = context.btxsetauthorvariant
361
362local ctx_btxstartauthor      = context.btxstartauthor
363local ctx_btxstopauthor       = context.btxstopauthor
364
365local ctx_btxciteauthorsetup  = context.btxciteauthorsetup
366local ctx_btxlistauthorsetup  = context.btxlistauthorsetup
367
368local concatstate = publications.concatstate
369local f_invalid   = formatters["<invalid %s: %s>"]
370
371local currentauthordata      = nil
372local currentauthorsymbol    = nil
373local currentauthorconnector = nil
374
375local manipulators       = typesetters.manipulators
376local splitmanipulation  = manipulators.splitspecification
377local applymanipulation  = manipulators.applyspecification
378
379local function value(i,field)
380    if currentauthordata then
381        local entry = currentauthordata[i]
382        if entry then
383            local value = entry[field]
384            if value and #value > 0 then
385                return value
386            end
387        end
388    end
389end
390
391implement { name = "btxcurrentfirstnames", arguments = "integer", actions = function(i) local v = value(i,"firstnames") if v then context(concat(v," ")) end end }
392implement { name = "btxcurrentinitials",   arguments = "integer", actions = function(i) local v = value(i,"initials")   if v then context(concat(the_initials(v,currentauthorsymbol,currentauthorconnector))) end end }
393implement { name = "btxcurrentjuniors",    arguments = "integer", actions = function(i) local v = value(i,"juniors")    if v then context(concat(v," ")) end end }
394implement { name = "btxcurrentsurnames",   arguments = "integer", actions = function(i) local v = value(i,"surnames")   if v then context(concat(v," ")) end end }
395implement { name = "btxcurrentvons",       arguments = "integer", actions = function(i) local v = value(i,"vons")       if v then context(concat(v," ")) end end }
396
397local function btxauthorfield(i,field)
398    if currentauthordata then
399        local entry = currentauthordata[i]
400        if entry then
401            local manipulator, field = splitmanipulation(field)
402            local value = entry[field]
403            if not value or #value == 0 then
404                -- value, no need for message
405            elseif manipulator then
406                for i=1,#value do
407                    if i > 1 then
408                        context(" ")
409                    end
410                    context(applymanipulation(manipulator,value) or value)
411                end
412            elseif field == "initials" then
413                context(concat(the_initials(value,currentauthorsymbol,currentauthorconnector)))
414            else
415                context(concat(value," "))
416            end
417        end
418    end
419end
420
421-- This is somewhat tricky: an author is not always an author but
422-- can also be a title or key, depending on the (optional) set it's
423-- in. Also, authors can be combined with years and so and they
424-- might be called upon mixed with other calls.
425
426local function btxauthor(dataset,tag,field,settings)
427    local split, usedfield, kind = getcasted(dataset,tag,field)
428    if kind == "author" then
429        local max = split and #split or 0
430        if max == 0 then
431            return
432            -- error
433        end
434        local absmax      = max
435        local etallimit   = tonumber(settings.etallimit) or 1000
436        local etaldisplay = tonumber(settings.etaldisplay) or etallimit
437        local etaloption  = settings_to_hash(settings.etaloption or "")
438        local etallast    = etaloption[v_last]
439        local combiner    = settings.combiner
440        local symbol      = settings.symbol
441        local connector   = settings.connector
442        local index       = settings.index
443        if not combiner or combiner == "" then
444            combiner = "normal"
445        end
446        if not symbol then
447            symbol = "."
448        end
449        local ctx_btxsetup = settings.kind == "cite" and ctx_btxciteauthorsetup or ctx_btxlistauthorsetup
450        if max > etallimit and (etaldisplay+(etallast and 1 or 0)) < max then
451            max = etaldisplay
452        else
453            etallast = false
454        end
455        currentauthordata      = split
456        currentauthorsymbol    = symbol
457        currentauthorconnector = connector
458
459        local function oneauthor(i,last,justone)
460            local author = split[i]
461            if index then
462                ctx_btxstartauthor(i,1,0)
463            elseif last then
464                ctx_btxstartauthor(i,1,0)
465                ctx_btxsetconcat(0)
466                ctx_btxsetauthorvariant(combiner)
467            else
468                local state = author.state or 0
469                ctx_btxstartauthor(i,max,state)
470                ctx_btxsetconcat(concatstate(i,max))
471                ctx_btxsetauthorvariant(combiner)
472            end
473            local initials = author.initials
474            if initials and #initials > 0 then
475                ctx_btxsetinitials() -- (concat(the_initials(initials,symbol)," "))
476            end
477            local firstnames = author.firstnames
478            if firstnames and #firstnames > 0 then
479                ctx_btxsetfirstnames() -- (concat(firstnames," "))
480            end
481            local vons = author.vons
482            if vons and #vons > 0 then
483                ctx_btxsetvons() -- (concat(vons," "))
484            end
485            local surnames = author.surnames
486            if surnames and #surnames > 0 then
487                ctx_btxsetsurnames() -- (concat(surnames," "))
488            end
489            local juniors = author.juniors
490            if juniors and #juniors > 0 then
491                ctx_btxsetjuniors() -- (concat(juniors," "))
492            end
493            if not index and i == max then
494                if split.others then
495                    ctx_btxsetoverflow(1)
496                else
497                    local overflow = #split - max
498                    if overflow > 0 then
499                        ctx_btxsetoverflow(overflow)
500                    end
501                end
502            end
503            ctx_btxsetup(combiner)
504            ctx_btxstopauthor()
505        end
506
507        if index then
508            oneauthor(index)
509        elseif max == 1 then
510            oneauthor(1,false,true)
511        else
512            for i=1,max do
513                oneauthor(i)
514            end
515            if etallast then
516                oneauthor(absmax,true)
517            end
518        end
519
520    else
521        report("ignored field %a of tag %a, used field %a is no author",field,tag,usedfield)
522    end
523
524end
525
526implement {
527    name      = "btxauthorfield",
528    actions   = btxauthorfield,
529    arguments = { "integer", "string" }
530}
531
532implement {
533    name      = "btxauthor",
534    actions   = btxauthor,
535    arguments = {
536        "argument",
537        "argument",
538        "argument",
539        {
540            { "combiner" },
541            { "kind" },
542            { "etallimit" },
543            { "etaldisplay" },
544            { "etaloption" },
545            { "symbol" },
546            { "connector" },
547        }
548    }
549}
550
551local function components(snippet,short)
552    local vons       = snippet.vons
553    local surnames   = snippet.surnames
554    local initials   = snippet.initials
555    local firstnames = not short and snippet.firstnames
556    local juniors    = snippet.juniors
557    return
558        vons       and #vons       > 0 and concat(vons," ") or "",
559        surnames   and #surnames   > 0 and concat(surnames," ") or "",
560        initials   and #initials   > 0 and concat(the_initials(initials)," ") or "",
561        firstnames and #firstnames > 0 and concat(firstnames," ") or "",
562        juniors    and #juniors    > 0 and concat(juniors, " ") or ""
563end
564
565local collapsers = allocate { }
566
567publications.authorcollapsers = collapsers
568
569-- making a constructor doesn't make the code nicer as the_initials is an
570-- exception
571
572local function default(author) -- one author
573    local hash = author.hash
574    if hash then
575        return hash
576    end
577    local original   = author.original
578    local vons       = author.vons
579    local surnames   = author.surnames
580    local initials   = author.initials
581    local firstnames = author.firstnames
582    local juniors    = author.juniors
583    local result     = { }
584    local nofresult  = 0
585    if vons and #vons > 0 then
586        for j=1,#vons do
587            nofresult = nofresult + 1
588            result[nofresult] = vons[j]
589        end
590    end
591    if surnames and #surnames > 0 then
592        for j=1,#surnames do
593            nofresult = nofresult + 1
594            result[nofresult] = surnames[j]
595        end
596    end
597    if initials and #initials > 0 then
598        initials = the_initials(initials)
599        for j=1,#initials do
600            nofresult = nofresult + 1
601            result[nofresult] = initials[j]
602        end
603    end
604    if firstnames and #firstnames > 0 then
605        for j=1,#firstnames do
606            nofresult = nofresult + 1
607            result[nofresult] = firstnames[j]
608        end
609    end
610    if juniors and #juniors > 0 then
611        for j=1,#juniors do
612            nofresult = nofresult + 1
613            result[nofresult] = juniors[j]
614        end
615    end
616    local hash = concat(result," ")
617    if trace_hashing then
618        report("hash: %s -> %s",original,hash)
619    end
620    author.hash = hash
621    return hash
622end
623
624local authorhashers        = { }
625publications.authorhashers = authorhashers
626
627-- todo: some hashing
628
629local function name(authors)
630    if type(authors) == "table" then
631        local n = #authors
632        if n == 0 then
633            return ""
634        end
635        local result    = { }
636        local nofresult = 0
637        for i=1,n do
638            local author   = authors[i]
639            local surnames = author.surnames
640            if surnames and #surnames > 0 then
641                for j=1,#surnames do
642                    nofresult = nofresult + 1
643                    result[nofresult] = surnames[j]
644                end
645            end
646        end
647        return concat(result," ")
648    else
649        return authors
650    end
651end
652
653table.setmetatableindex(authorhashers,function(t,k)
654    t[k] = name
655    return name
656end)
657
658authorhashers.normal = function(authors)
659    if type(authors) == "table" then
660        local n = #authors
661        if n == 0 then
662            return ""
663        end
664        local result    = { }
665        local nofresult = 0
666        for i=1,n do
667            local author     = authors[i]
668            local vons       = author.vons
669            local surnames   = author.surnames
670            local firstnames = author.firstnames
671            local juniors    = author.juniors
672            if vons and #vons > 0 then
673                for j=1,#vons do
674                    nofresult = nofresult + 1
675                    result[nofresult] = vons[j]
676                end
677            end
678            if surnames and #surnames > 0 then
679                for j=1,#surnames do
680                    nofresult = nofresult + 1
681                    result[nofresult] = surnames[j]
682                end
683            end
684            if firstnames and #firstnames > 0 then
685                for j=1,#firstnames do
686                    nofresult = nofresult + 1
687                    result[nofresult] = firstnames[j]
688                end
689            end
690            if juniors and #juniors > 0 then
691                for j=1,#juniors do
692                    nofresult = nofresult + 1
693                    result[nofresult] = juniors[j]
694                end
695            end
696        end
697        return concat(result," ")
698    else
699        return authors
700    end
701end
702
703authorhashers.normalshort = function(authors)
704    if type(authors) == "table" then
705        local n = #authors
706        if n == 0 then
707            return ""
708        end
709        local result    = { }
710        local nofresult = 0
711        for i=1,n do
712            local author   = authors[i]
713            local vons     = author.vons
714            local surnames = author.surnames
715            local initials = author.initials
716            local juniors  = author.juniors
717            if vons and #vons > 0 then
718                for j=1,#vons do
719                    nofresult = nofresult + 1
720                    result[nofresult] = vons[j]
721                end
722            end
723            if surnames and #surnames > 0 then
724                for j=1,#surnames do
725                    nofresult = nofresult + 1
726                    result[nofresult] = surnames[j]
727                end
728            end
729            if initials and #initials > 0 then
730                initials = the_initials(initials)
731                for j=1,#initials do
732                    nofresult = nofresult + 1
733                    result[nofresult] = initials[j]
734                end
735            end
736            if juniors and #juniors > 0 then
737                for j=1,#juniors do
738                    nofresult = nofresult + 1
739                    result[nofresult] = juniors[j]
740                end
741            end
742        end
743        return concat(result," ")
744    else
745        return authors
746    end
747end
748
749local sequentialhash = function(authors)
750    if type(authors) == "table" then
751        local n = #authors
752        if n == 0 then
753            return ""
754        end
755        local result    = { }
756        local nofresult = 0
757        for i=1,n do
758            local author     = authors[i]
759            local vons       = author.vons
760            local surnames   = author.surnames
761            local firstnames = author.firstnames
762            local juniors    = author.juniors
763            if firstnames and #firstnames > 0 then
764                for j=1,#firstnames do
765                    nofresult = nofresult + 1
766                    result[nofresult] = firstnames[j]
767                end
768            end
769            if vons and #vons > 0 then
770                for j=1,#vons do
771                    nofresult = nofresult + 1
772                    result[nofresult] = vons[j]
773                end
774            end
775            if surnames and #surnames > 0 then
776                for j=1,#surnames do
777                    nofresult = nofresult + 1
778                    result[nofresult] = surnames[j]
779                end
780            end
781            if juniors and #juniors > 0 then
782                for j=1,#juniors do
783                    nofresult = nofresult + 1
784                    result[nofresult] = juniors[j]
785                end
786            end
787        end
788        return concat(result," ")
789    else
790        return authors
791    end
792end
793
794local sequentialshorthash = function(authors)
795    if type(authors) == "table" then
796        local n = #authors
797        if n == 0 then
798            return ""
799        end
800        local result    = { }
801        local nofresult = 0
802        for i=1,n do
803            local author   = authors[i]
804            local vons     = author.vons
805            local surnames = author.surnames
806            local initials = author.initials
807            local juniors  = author.juniors
808            if initials and #initials > 0 then
809                initials = the_initials(initials)
810                for j=1,#initials do
811                    nofresult = nofresult + 1
812                    result[nofresult] = initials[j]
813                end
814            end
815            if vons and #vons > 0 then
816                for j=1,#vons do
817                    nofresult = nofresult + 1
818                    result[nofresult] = vons[j]
819                end
820            end
821            if surnames and #surnames > 0 then
822                for j=1,#surnames do
823                    nofresult = nofresult + 1
824                    result[nofresult] = surnames[j]
825                end
826            end
827            if juniors and #juniors > 0 then
828                for j=1,#juniors do
829                    nofresult = nofresult + 1
830                    result[nofresult] = juniors[j]
831                end
832            end
833        end
834        return concat(result," ")
835    else
836        return authors
837    end
838end
839
840authorhashers.sequential      = sequentialhash
841authorhashers.sequentialshort = sequentialshorthash
842authorhashers.normalinverted  = authorhashers.normal
843authorhashers.invertedshort   = authorhashers.normalshort
844
845local p_clean = Cs ( (
846                    P("\\btxcmd") / "" -- better keep the argument
847                  + S("`~!@#$%^&*()_-+={}[]:;\"\'<>,.?/|\\") / ""
848                  + lpeg.patterns.utf8character
849                )^1)
850
851-- Probabbly more robust is a two pass approach.
852
853authorhashers.short = function(authors)
854    -- a short is a real dumb hardcoded kind of tag and we only support
855    -- this one because some users might expect it, not because it makes
856    -- sense
857    if type(authors) == "table" then
858        local n = #authors
859        if n == 0 then
860            return "unk"
861        elseif n == 1 then
862            local surnames = authors[1].surnames
863            if not surnames or #surnames == 0 then
864                return "err"
865            else
866                local s = surnames[1]
867                local c = lpegmatch(p_clean,s)
868                if trace_hashing and s ~= c then
869                    report_cite("name %a cleaned to %a for short construction",s,c)
870                end
871                return utfsub(c,1,3)
872            end
873        else
874            local t = { }
875            for i=1,n do
876                if i > 3 then
877                    t[#t+1] = "+" -- indeed
878                    break
879                end
880                local surnames = authors[i].surnames
881                if not surnames or #surnames == 0 then
882                    t[#t+1] = "?"
883                else
884                    local s = surnames[1]
885                    local c = lpegmatch(p_clean,s)
886                    if s ~= c then
887                        report_cite("name %a cleaned to %a for short construction",s,c)
888                    end
889                    t[#t+1] = utfsub(c,1,1)
890                end
891            end
892            return concat(t)
893        end
894    else
895        return utfsub(authors,1,3)
896    end
897end
898
899collapsers.default = default
900
901local function authorwriter(key,index)
902    if not key then
903        return ""
904    end
905    if type(key) == "string" then
906        return key
907    end
908    local n = #key
909    if n == 0 then
910        return ""
911    end
912    if index then
913        if not key[index] then
914            return ""
915        end
916    elseif n == 1 then
917        index = 1
918    end
919    if index then
920        local author  = key[index]
921        local options = author.options
922        if options then
923            for option in next, options do
924                local collapse = collapsers[option]
925                if collapse then
926                    return collapse(author)
927                end
928            end
929        end
930        local hash = default(author)
931     -- if trace_hashing then
932     --     report("hash: %s",hash)
933     -- end
934        return hash
935    end
936    local t = { }
937    local s = 0
938    for i=1,n do
939        local author  = key[i]
940        local options = author.options
941        s = s + 1
942        if options then
943            local done = false
944            for option in next, options do
945                local collapse = collapsers[option]
946                if collapse then
947                    t[s] = collapse(author)
948                    done = true
949                end
950            end
951            if not done then
952                t[s] = default(author)
953            end
954        else
955            t[s] = default(author)
956        end
957    end
958    local hash = concat(t," & ")
959 -- if trace_hashing then
960 --     report("hash: %s",hash)
961 -- end
962    return hash
963end
964
965local function writer(key)
966    return authorwriter(key) -- discard extra arguments in the caller
967end
968
969publications.writers   .author = writer
970publications.casters   .author = splitauthorstring
971publications.components.author = components
972
973-- sharedmethods.author = {
974--     { field = "key",     default = "",     unknown = "" },
975--     { field = "author",  default = "",     unknown = "" },
976--     { field = "title",   default = "",     unknown = "" },
977-- }
978
979-- Analysis of the APA by Alan:
980--
981-- first : key author editor publisher title           journal volume number pages
982-- second: year suffix                 title month day journal volume number
983
984publications.sortmethods.authoryear = {
985    sequence = {
986     -- { field = "key",     default = "ZZZZ", unknown = "ZZZZ" },
987        { field = "author",  default = "",     unknown = "" },
988        { field = "year",    default = "9998", unknown = "9999" },
989     -- { field = "suffix",  default = " ",    unknown = " " },
990        { field = "month",   default = "13",   unknown = "14" },
991        { field = "day",     default = "32",   unknown = "33" },
992        { field = "journal", default = "",     unknown = "" },
993        { field = "volume",  default = "",     unknown = "" },
994     -- { field = "number",  default = "",     unknown = "" },
995        { field = "pages",   default = "",     unknown = "" },
996        { field = "title",   default = "",     unknown = "" },
997        { field = "index",   default = "",     unknown = "" },
998    },
999}
1000
1001publications.sortmethods.authortitle = {
1002    sequence = {
1003        { field = "author",    default = "",           unknown = "" },
1004        { field = "title",     default = "",           unknown = "" },
1005        { field = "booktitle", default = "",           unknown = "" }, -- if this is an untitled section (e.g., introduction, foreword, preface) of a book or a review of a book
1006        { field = "maintitle", default = "",           unknown = "" }, -- if this is an untitled section or volume in a multivolume collection
1007        { field = "volume",    default = "",           unknown = "" },
1008        { field = "part",      default = "",           unknown = "" },
1009        { field = "date",      default = "9998-13-32", unknown = "9999-14-33" }, -- some specifications allow date instead of year, month, day
1010        { field = "year",      default = "9998",       unknown = "9999" },
1011        { field = "month",     default = "13",         unknown = "14" },
1012        { field = "day",       default = "32",         unknown = "33" },
1013        { field = "index",     default = "",           unknown = "" },
1014    },
1015}
1016
1017implement {
1018    name      = "btxremapauthor",
1019    arguments = "2 strings",
1020    actions   = function(k,v)
1021        local a  = { splitauthor(k,true) }
1022        local s1 = sequentialhash(a)
1023        local s2 = sequentialshorthash(a)
1024        if not authormap[k] then
1025            authormap[k] = v
1026            report("%a mapped onto %a",k,v)
1027        end
1028        if not authormap[s1] then
1029            authormap[s1] = v
1030            report("%a mapped onto %a, derived from %a",s1,v,k)
1031        end
1032        if not authormap[s2] then
1033            authormap[s2] = v
1034            report("%a mapped onto %a, derived from %a",s2,v,k)
1035        end
1036    end
1037}
1038
1039implement {
1040    name      = "btxshowauthorremapping",
1041    actions   = function(k,v)
1042        report("start author remapping")
1043        for k, v in sortedhash(authormap) do
1044            report(" %s => %s",k,v)
1045        end
1046        report("stop author remapping")
1047    end
1048}
1049