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