font-sel.lua /size: 33 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['font-sel'] = {
2    version   = 1.001,
3    comment   = "companion to font-sel.mkvi",
4    author    = "Wolfgang Schuster",
5    copyright = "Wolfgang Schuster",
6    license   = "GNU General Public License"
7}
8
9local next, type = next, type
10
11local context                    = context
12local cleanname                  = fonts.names.cleanname
13local gsub, splitup, find, lower = string.gsub, string.splitup, string.find, string.lower
14local concat, sortedkeys         = table.concat, table.sortedkeys
15local merge, remove              = table.merge, table.remove
16local splitbase, removesuffix    = file.splitbase, file.removesuffix
17local splitat, lpegmatch         = lpeg.splitat, lpeg.match
18
19local formatters                 = string.formatters
20local settings_to_array          = utilities.parsers.settings_to_array
21local settings_to_hash           = utilities.parsers.settings_to_hash
22local allocate                   = utilities.storage.allocate
23
24local v_default                  = interfaces.variables.default
25
26local implement                  = interfaces.implement
27
28local fonts                      = fonts
29
30local getlookups                 = fonts.names.getlookups
31local registerdesignsizes        = fonts.goodies.designsizes.register
32local bodyfontsizes              = storage.shared.bodyfontsizes
33
34fonts.select                     = fonts.select or { }
35local selectfont                 = fonts.select
36
37selectfont.data                  = selectfont.data         or allocate()
38selectfont.fallbacks             = selectfont.fallbacks    or allocate()
39selectfont.methods               = selectfont.methods      or allocate()
40selectfont.extras                = selectfont.extras       or allocate()
41selectfont.alternatives          = selectfont.alternatives or allocate()
42selectfont.presets               = selectfont.presets      or allocate()
43selectfont.defaults              = selectfont.defaults     or allocate()
44
45storage.register("fonts/select/presets", selectfont.presets, "fonts.select.presets")
46
47local data                       = selectfont.data
48local fallbacks                  = selectfont.fallbacks
49local methods                    = selectfont.methods
50local extras                     = selectfont.extras
51local alternatives               = selectfont.alternatives
52local presets                    = selectfont.presets
53local defaults                   = selectfont.defaults
54
55local ctx_definefontsynonym      = context.definefontsynonym
56local ctx_resetfontfallback      = context.resetfontfallback
57local ctx_startfontclass         = context.startfontclass
58local ctx_stopfontclass          = context.stopfontclass
59local ctx_loadfontgoodies        = context.loadfontgoodies
60local ctx_definefontfallback     = context.definefontfallback
61local ctx_definetypeface         = context.definetypeface
62local ctx_definebodyfont         = context.definebodyfont
63
64local trace_register     = false  trackers.register("selectfont.register",     function(v) trace_register     = v end)
65local trace_files        = false  trackers.register("selectfont.files",        function(v) trace_files        = v end)
66local trace_features     = false  trackers.register("selectfont.features",     function(v) trace_features     = v end)
67local trace_goodies      = false  trackers.register("selectfont.goodies",      function(v) trace_goodies      = v end)
68local trace_alternatives = false  trackers.register("selectfont.alternatives", function(v) trace_alternatives = v end)
69local trace_typescript   = false  trackers.register("selectfont.typescripts",  function(v) trace_typescript   = v end)
70
71local report_selectfont   = logs.reporter("selectfont")
72local report_files        = logs.reporter("selectfont","files")
73local report_features     = logs.reporter("selectfont","features")
74local report_goodies      = logs.reporter("selectfont","goodies")
75local report_typescript   = logs.reporter("selectfont","typescripts")
76
77defaults["rm"] = { features = { ["sc"] = "*,f:smallcaps" } }
78defaults["ss"] = { features = { ["sc"] = "*,f:smallcaps" } }
79
80-- WS: we need to check the extras with the typescripts
81
82defaults["asanamath"]          = { options = { extras = "asana-math",           features = "math\\mathsizesuffix,mathextra",         goodies = "anana-math"   } }
83defaults["cambriamath"]        = { options = { extras = "cambria-math",         features = "math\\mathsizesuffix,mathextra",         goodies = "cambria-math" } }
84defaults["euler"]              = { options = { extras = "euler-math",           features = "math\\mathsizesuffix,mathextra",         goodies = "euler-math"   } }
85defaults["latinmodernmath"]    = { options = { extras = "lm,lm-math",           features = "math\\mathsizesuffix,mathextra,lm-math", goodies = "modern"       } }
86defaults["lucidabrightmathot"] = { options = { extras = "lucida-opentype-math", features = "math\\mathsizesuffix,mathextra",         goodies = "lucida-math"  } }
87defaults["minionmath"]         = { options = { extras = "minion-math",          features = "math\\mathsizesuffix,mathextra",         goodies = "minion-math"  } }
88defaults["stixtwomath"]        = { options = { extras = "stix-two-math",        features = "math\\mathsizesuffix,mathextra",         goodies = "stixtwo-math" } }
89defaults["texgyredejavumath"]  = { options = { extras = "dejavu",               features = "math\\mathsizesuffix,mathextra",         goodies = "dejavu-math"  } }
90defaults["texgyrepagellamath"] = { options = { extras = "texgyre",              features = "math\\mathsizesuffix,mathextra",         goodies = "pagella-math" } }
91defaults["texgyrebonummath"]   = { options = { extras = "texgyre",              features = "math\\mathsizesuffix,mathextra",         goodies = "bonum-math"   } }
92defaults["texgyrescholamath"]  = { options = { extras = "texgyre",              features = "math\\mathsizesuffix,mathextra",         goodies = "schola-math"  } }
93defaults["texgyretermesmath"]  = { options = { extras = "texgyre",              features = "math\\mathsizesuffix,mathextra",         goodies = "termes-math"  } }
94defaults["xitsmath"]           = { options = { extras = "xits-math",            features = "math\\mathsizesuffix,mathextra",         goodies = "xits-math"    } }
95
96defaults["neoeuler"]           = defaults["euler"]
97defaults["dejavumath"]         = defaults["texgyredejavumath"]
98
99extras["features"] = function(data,alternative,features)
100    local d = data.options.features
101    local e = gsub(gsub(features,"*",d),"{(.*)}","%1")
102    local f = data.features
103    if trace_features then
104        report_features("alternative '%s': saving features '%s'",alternative,e)
105    end
106    if not f then
107        f = { }
108        data.features = f
109    end
110    f[alternative] = e
111end
112
113extras["goodies"] = function(data,alternative,goodies)
114    local e = gsub(goodies,"{(.*)}","%1")
115    local g = data.goodies
116    if trace_goodies then
117        report_goodies("alternative '%s': saving goodies '%s'",alternative,e)
118    end
119    if not g then
120        g = { }
121        data.goodies = g
122    end
123    g[alternative] = e
124end
125
126local function selectfont_savefile(data,alternative,bodyfontsize,size,file)
127    local f    = data.files
128    local p, n = splitbase(file["filename"])
129    local t    = file["format"]
130    local r    = file["rawname"]
131    if t == "ttc" then
132        n = formatters["%s(%s)"](n,r)
133    end
134    if not f then
135        f = { }
136        data.files = f
137    end
138    local a = f[alternative]
139    if not a then
140        a = { }
141        f[alternative] = a
142    end
143    a[bodyfontsize] = { size, n }
144    if trace_files then
145        report_files("alternative '%s': saving file '%s' for size '%s'",alternative,n,size)
146    end
147end
148
149methods["name"] = function(data,alternative,name)
150    local family   = data.metadata.family
151    local filename = cleanname(gsub(name,"*",family))
152    if trace_alternatives then
153        report_selectfont("alternative '%s': using method 'name' with argument '%s'",alternative,filename)
154    end
155    local fontname = getlookups{ fontname = filename }
156    local fullname = getlookups{ fullname = filename }
157    if #fontname > 0 then
158        selectfont_savefile(data,alternative,0,"default",fontname[1])
159    elseif #fullname > 0 then
160        selectfont_savefile(data,alternative,0,"default",fullname[1])
161    else
162        if trace_alternatives then
163            report_selectfont("alternative '%s': no font was found for the requested name '%s'",alternative,filename)
164        end
165    end
166end
167
168methods["file"] = function(data,alternative,file)
169    local family   = data.metadata.family
170    local filename = cleanname(gsub(removesuffix(file),"*",family))
171    if trace_alternatives then
172        report_selectfont("alternative '%s': using method 'file' with argument '%s'",alternative,filename)
173    end
174    local filename = getlookups{ cleanfilename = cleanname(filename) }
175    if #filename > 0 then
176        selectfont_savefile(data,alternative,0,"default",filename[1])
177    else
178        if trace_alternatives then
179            report_selectfont("alternative '%s': no font was found for the requested file '%s'",alternative,cleanname(gsub(removesuffix(file),"*",family)))
180        end
181    end
182end
183
184local m_weight = {
185    ["thin"]       = 100,
186    ["extralight"] = 200,
187    ["light"]      = 300,
188    ["regular"]    = 400,
189    ["medium"]     = 500,
190    ["semibold"]   = 600,
191    ["bold"]       = 700,
192    ["extrabold"]  = 800,
193    ["black"]      = 900
194}
195
196local m_width = {
197    ["ultracondensed"] = 1,
198    ["extracondensed"] = 2,
199    ["condensed"]      = 3,
200    ["semicondensed"]  = 4,
201    ["normal"]         = 5,
202    ["semiexpanded"]   = 6,
203    ["expanded"]       = 7,
204    ["extraexpanded"]  = 8,
205    ["ultraexpanded"]  = 9,
206}
207
208local m_name = {
209    ["thin"]             = { weight = "thin"                                       },
210    ["thinitalic"]       = { weight = "thin",                  style = "italic"    },
211    ["extralight"]       = { weight = "extralight"                                 },
212    ["extralightitalic"] = { weight = "extralight",            style = "italic"    },
213    ["light"]            = { weight = "light"                                      },
214    ["lightitalic"]      = { weight = "light",                 style = "italic"    },
215    ["regular"]          = { weight = { "regular", "medium" }                      },
216    ["italic"]           = { weight = { "regular", "medium" }, style = "italic"    },
217    ["medium"]           = { weight = "medium"                                     },
218    ["mediumitalic"]     = { weight = "medium",                style = "italic"    },
219    ["semibold"]         = { weight = "semibold"                                   },
220    ["semibolditalic"]   = { weight = "semibold",              style = "italic"    },
221    ["bold"]             = { weight = { "bold", "semibold" }                       },
222    ["bolditalic"]       = { weight = { "bold", "semibold" },  style = "italic"    },
223    ["extrabold"]        = { weight = "extrabold"                                  },
224    ["extrabolditalic"]  = { weight = "extrabold",             style = "italic"    },
225    ["black"]            = { weight = "black"                                      },
226    ["blackitalic"]      = { weight = "black",                 style = "italic"    },
227    ["smallcaps"]        = { weight = "regular",             variant = "smallcaps" },
228}
229
230local m_alternative = {
231    ["tf"] = "regular",
232    ["bf"] = "bold",
233    ["it"] = "italic",
234    ["sl"] = "italic",
235    ["bi"] = "bolditalic",
236    ["bs"] = "bolditalic",
237    ["sc"] = "smallcaps"
238}
239
240local function m_style_family(family)
241    local askedname  = cleanname(family)
242    local familyname = getlookups{ familyname = askedname }
243    local family     = getlookups{ family     = askedname }
244    local fontname   = getlookups{ fontname   = askedname }
245    if #familyname > 0 then
246        return familyname
247    elseif #family > 0 then
248        return family
249    elseif #fontname > 0 then
250        local fontfamily = fontname[1]["familyname"]
251        report_selectfont("the name '%s' is not a proper family name, use '%s' instead.",askedname,fontfamily)
252        return nil
253    else
254        return nil
255    end
256end
257
258local function m_style_subfamily(entries,style,family)
259    local t      = { }
260    local style  = cleanname(style)
261    local family = cleanname(family)
262    for index, entry in next, entries do
263        if entry["familyname"] == family and entry["subfamilyname"] == style then -- familyname + subfamilyname
264            t[#t+1] = entry
265        elseif entry["family"] == family and entry["subfamily"] == style then -- family + subfamily
266            t[#t+1] = entry
267        end
268    end
269    return #t ~= 0 and t or nil
270end
271
272local function m_style_weight(entries,style)
273    local t = { }
274    local weight    = m_name[style] and m_name[style]["weight"] or "regular"
275    if type(weight) == "table" then
276        for _, w in next, weight do
277            local found = false
278            local pfmweight = m_weight[w]
279            for index, entry in next, entries do
280                if entry["pfmweight"] == pfmweight then
281                    found = true
282                    t[#t+1] = entry
283                elseif entry["weight"] == w then
284                    found = true
285                    t[#t+1] = entry
286                end
287            end
288            if found then break end
289        end
290    else
291        local pfmweight = m_weight[weight]
292        for index, entry in next, entries do
293            if entry["pfmweight"] == pfmweight then
294                t[#t+1] = entry
295            elseif entry["weight"] == weight then
296                t[#t+1] = entry
297            end
298        end
299    end
300    return #t ~= 0 and t or nil
301end
302
303local function m_style_style(entries,style)
304    local t = { }
305    local style = m_name[style] and m_name[style]["style"] or "normal"
306    for index, entry in next, entries do
307        if style == "italic" and entry["angle"] and entry["angle"] ~= 0 then
308            t[#t+1] = entry
309        elseif style == "normal" and entry["angle"] and entry["angle"] ~= 0 then
310         -- Fix needed for fonts with wrong value for the style field
311        elseif entry["style"] == style then
312            t[#t+1] = entry
313        end
314    end
315    return #t ~= 0 and t or nil
316end
317
318local function m_style_variant(entries,style)
319    local t = { }
320    local variant = m_name[style] and m_name[style]["variant"] or "normal"
321    for index, entry in next, entries do
322        if entry["variant"] == variant then
323            t[#t+1] = entry
324        end
325    end
326    return #t ~= 0 and t or nil
327end
328
329local function m_style_width(entries,style)
330    local t = { }
331    local width    = m_name[style] and m_name[style]["width"] or "normal"
332    local pfmwidth = m_width[width]
333    for index, entry in next, entries do
334        if entry["pfmwidth"] == pfmwidth then
335            t[#t+1] = entry
336        end
337    end
338    return #t ~= 0 and t or nil
339end
340
341local function m_style_size(data,alternative,entries)
342    if #entries == 1 then
343        selectfont_savefile(data,alternative,0,"default",entries[1])
344    else
345        for index, entry in next, entries do
346            local minsize = entry["minsize"]
347            local maxsize = entry["maxsize"]
348            if minsize and maxsize then
349                for size, state in next, bodyfontsizes do
350                    local bodyfontsize, _ = number.splitdimen(size)
351                          bodyfontsize    = bodyfontsize * 10
352                    if minsize < bodyfontsize and bodyfontsize < maxsize then
353                        if bodyfontsize == 100 then
354                            selectfont_savefile(data,alternative,0,"default",entry)
355                        end
356                        selectfont_savefile(data,alternative,bodyfontsize,size,entry)
357                    end
358                end
359            else
360                if trace_alternatives then
361                    report_selectfont("alternative '%s': multiple files are available for the requested style '%s' from '%s'",alternative,style,family)
362                end
363            end
364        end
365    end
366end
367
368methods["style"] = function(data,alternative,style)
369    local fontfamily = data.metadata.family
370    local designsize = data.options.designsize
371    local fontstyle  = m_alternative[style] or style
372    local entries    = m_style_family(fontfamily)
373    if entries then
374        local subfamily = m_style_subfamily(entries,fontstyle,fontfamily)
375        if subfamily then
376            entries = subfamily
377        else
378            entries = m_style_weight(entries,fontstyle)
379            if entries then
380                entries = m_style_style(entries,fontstyle)
381                if entries then
382                    entries = m_style_variant(entries,fontstyle)
383                    if entries and #entries > 1 and designsize == "default" then
384                        entries = m_style_width(entries,fontstyle)
385                    end
386                end
387            end
388        end
389    end
390    if entries then
391        m_style_size(data,alternative,entries)
392    else
393        if trace_alternatives then
394            report_selectfont("alternative '%s': no font was found for the requested style '%s' from '%s'",alternative,style,family)
395        end
396    end
397end
398
399methods[v_default] = function(data,alternative)
400    local family = data.metadata.family
401    if trace_alternatives then
402        report_selectfont("alternative '%s': using method 'default'",alternative)
403    end
404    local result = getlookups{ familyname = cleanname(family) }
405    if #result == 1 and alternative == "tf" then
406        if trace_alternatives then
407            report_selectfont("alternative '%s': the family '%s' contains only one font",alternative,family)
408        end
409        selectfont_savefile(data,alternative,0,"default",result[1])
410     -- if trace_alternatives then
411     --     report_selectfont("alternative '%s': changing method 'default' to method 'style'",alternative)
412     -- end
413     -- methods["file"](data,alternative,result[1]["filename"])
414    else
415        if trace_alternatives then
416            report_selectfont("alternative '%s': changing method 'default' to method 'style'",alternative)
417        end
418        methods["style"](data,alternative,alternative)
419    end
420end
421
422local function selectfont_savealternative(data,alternative,userdata)
423    local a = data.alternatives
424    local e = userdata[alternative]
425    if not a then
426        a = { }
427        data.alternatives = a
428    end
429    a[alternative] = e
430end
431
432function selectfont.fontdata(index)
433    local data     = data[index]
434    local style    = data.metadata.style
435    local defaults = defaults[style]
436    if defaults then
437        for category, argument in next, defaults do
438            local extra  = extras[category]
439            if extra then
440                for alternative, entry in next, argument do
441                    extra(data,alternative,entry)
442                end
443            end
444        end
445    end
446end
447
448function selectfont.userdata(index)
449    local data     = data[index]
450    local preset   = data.options.preset
451    local presets  = presets[preset]
452    local userdata = settings_to_hash(data.userdata)
453    if presets then
454        merge(userdata,presets)
455    end
456    for alternative, _ in next, alternatives do
457        selectfont_savealternative(data,alternative,userdata)
458    end
459end
460
461function selectfont.registerfiles(index)
462    local data  = data[index]
463    local colon = splitat(":",true)
464    for alternative, _ in next, alternatives do
465        local arguments = data.alternatives[alternative]
466        if arguments and arguments ~= "" then
467            local entries = settings_to_array(arguments)
468            for index, entry in next, entries do
469                method, argument = lpegmatch(colon,entry)
470                if not argument then
471                    argument = method
472                    method   = "name"
473                end
474                if #entries == 1 and method == "features" then
475                    extras["features"](data,alternative,argument)
476                    methods[v_default](data,alternative)
477                else
478                    (extras[method] or methods[method] or methods[v_default])(data,alternative,argument)
479                end
480            end
481        else
482            methods[v_default](data,alternative)
483        end
484    end
485end
486
487function selectfont.registerfontalternative(alternative)
488    local a = alternatives[alternative]
489    if not a then
490        if trace_register then
491            report_selectfont("register alternative '%s'",alternative)
492        end
493        a = true
494        alternatives[alternative] = a
495    end
496end
497
498function selectfont.registerfallback(index)
499    local data      = data[index]
500    local fontclass = data.metadata.typeface
501    local fontstyle = data.metadata.style
502    local fallback  = fallbacks[fontclass]
503    if not fallback then
504        fallback = { }
505        fallbacks[fontclass] = fallback
506    end
507    local entries = fallback[fontstyle]
508    if not entries then
509        entries = { }
510        fallback[fontstyle] = entries
511    end
512    entries[#entries+1] = index
513end
514
515function selectfont.registerfontfamily(settings)
516    local index = #data + 1
517    data[index] = settings
518    selectfont.fontdata     (index)
519    selectfont.userdata     (index)
520    selectfont.registerfiles(index)
521    return index
522end
523
524local m_synonym = {
525    ["rm"] = {
526        ["tf"] = "Serif",
527        ["bf"] = "SerifBold",
528        ["it"] = "SerifItalic",
529        ["sl"] = "SerifSlanted",
530        ["bi"] = "SerifBoldItalic",
531        ["bs"] = "SerifBoldSlanted",
532        ["sc"] = "SerifCaps",
533    },
534    ["ss"] = {
535        ["tf"] = "Sans",
536        ["bf"] = "SansBold",
537        ["it"] = "SansItalic",
538        ["sl"] = "SansSlanted",
539        ["bi"] = "SansBoldItalic",
540        ["bs"] = "SansBoldSlanted",
541        ["sc"] = "SansCaps",
542    },
543    ["tt"] = {
544        ["tf"] = "Mono",
545        ["bf"] = "MonoBold",
546        ["it"] = "MonoItalic",
547        ["sl"] = "MonoSlanted",
548        ["bi"] = "MonoBoldItalic",
549        ["bs"] = "MonoBoldSlanted",
550        ["sc"] = "MonoCaps",
551    },
552    ["mm"] = {
553        ["tf"] = "MathRoman",
554        ["bf"] = "MathBold",
555    },
556    ["hw"] = {
557        ["tf"] = "Handwriting",
558    },
559    ["cg"] = {
560        ["tf"] = "Calligraphy",
561    },
562}
563
564function selectfont.features(data,style,alternative)
565    local family   = data.metadata.family
566    local features = data.features
567    local options  = data.options
568    local defaults = defaults[cleanname(family)]
569    if features and features[alternative] then
570        return features[alternative]
571    elseif defaults and defaults.options and defaults.options.features then
572        return defaults.options.features
573    else
574        return options.features
575    end
576end
577
578function selectfont.goodies(data,style,alternative)
579    local family   = data.metadata.family
580    local goodies  = data.goodies
581    local options  = data.options
582    local defaults = defaults[cleanname(family)]
583    if goodies and goodies[alternative] then
584        return goodies[alternative]
585    elseif defaults and defaults.options and defaults.options.goodies then
586        return defaults.options.goodies
587    else
588        return options.goodies
589    end
590end
591
592function selectfont.fontsynonym(data,class,style,alternative,index)
593    local fontfiles    = data.files[alternative] or data.files["tf"]
594    local fontsizes    = sortedkeys(fontfiles)
595    local fallback     = index ~= 0
596    local fontclass    = lower(class)
597  --local fontfeature  = data.features and data.features[alternative] or data.options.features
598  --local fontgoodie   = data.goodies  and data.goodies [alternative] or data.options.goodies
599    local fontfeature  = selectfont.features(data,style,alternative)
600    local fontgoodie   = selectfont.goodies (data,style,alternative)
601    local synonym      = m_synonym[style] and m_synonym[style][alternative]
602    local fontfile     = formatters    ["file-%s-%s-%s"](fontclass,style,alternative)
603    local fontsynonym  = formatters ["synonym-%s-%s-%s"](fontclass,style,alternative)
604    if fallback then
605        fontfile     = formatters    ["file-%s-%s-%s-%s"](fontclass,style,alternative,index)
606        fontsynonym  = formatters ["synonym-%s-%s-%s-%s"](fontclass,style,alternative,index)
607    end
608    local fontfallback = formatters["fallback-%s-%s-%s"](fontclass,style,alternative)
609    for _, fontsize in next, fontsizes do
610     -- if trace_typescript then
611     --     report_typescript("synonym: '%s', size: '%s', file: '%s'",fontfile,fontfiles[fontsize][1],fontfiles[fontsize][2])
612     -- end
613        registerdesignsizes(fontfile,fontfiles[fontsize][1],fontfiles[fontsize][2])
614    end
615    if fallback then
616     -- if trace_typescript then
617     --     report_typescript("synonym: '%s', file: '%s', features: '%s'",fontsynonym,fontfile,fontfeature)
618     -- end
619        ctx_definefontsynonym( { fontsynonym }, { fontfile }, { features = fontfeature } )
620    else
621     -- if trace_typescript then
622     --     report_typescript("synonym: '%s', file: '%s', features: '%s', goodies: '%s', fallbacks: '%s'",fontsynonym,fontfile,fontfeature,fontgoodie,fontfallback)
623     -- end
624        ctx_definefontsynonym( { fontsynonym }, { fontfile }, { features = fontfeature, goodies = fontgoodie, fallbacks = fontfallback } )
625        if synonym then
626         -- if trace_typescript then
627         --     report_typescript("synonym: '%s', file: '%s'",synonym,fontsynonym)
628         -- end
629            ctx_definefontsynonym( { synonym }, { fontsynonym } )
630        end
631    end
632end
633
634function selectfont.fontfallback(data,class,style,alternative,index)
635    local range        = data.options.range
636    local scale        = data.options.rscale ~= "" and data.options.rscale or 1
637    local check        = data.options.check  ~= "" and data.options.check  or ""
638    local force        = data.options.force  ~= "" and data.options.force  or ""
639    local fontfeature  = data.features and data.features[alternative] or data.options.features
640    local fontclass    = lower(class)
641    local fontsynonym  = formatters["synonym-%s-%s-%s-%s"](fontclass,style,alternative,index)
642    local fontfallback = formatters["fallback-%s-%s-%s"]  (fontclass,style,alternative)
643    if index == 1 then
644        ctx_resetfontfallback( { fontfallback } )
645    end
646 -- if trace_typescript then
647 --     report_typescript("fallback: '%s', synonym: '%s', range: '%s', scale: '%s', check: '%s', force: '%s'",fontfallback,fontsynonym,range,scale,check,force)
648 -- end
649    ctx_definefontfallback( { fontfallback }, { fontsynonym }, { range }, { rscale = scale, check = check, force = force } )
650end
651
652function selectfont.filefallback(data,class,style,alternative,index)
653    local range        = data.options.range
654    local offset       = data.options.offset
655    local scale        = data.options.rscale ~= "" and data.options.rscale or 1
656    local check        = data.options.check  ~= "" and data.options.check  or "yes"
657    local force        = data.options.force  ~= "" and data.options.force  or "yes"
658    local fontfile     = data.files[alternative] and data.files[alternative][0] or data.files["tf"][0]
659    local fontfeature  = data.features and data.features[alternative] or data.options.features
660    local fontclass    = lower(class)
661    local fontfallback = formatters["fallback-%s-%s-%s"](fontclass,style,alternative)
662    if index == 1 then
663        ctx_resetfontfallback( { fontfallback } )
664    end
665 -- if trace_typescript then
666 --     report_typescript("fallback: '%s', file: '%s', features: '%s', range: '%s', scale: '%s', check: '%s', force: '%s', offset: '%s'",fontfallback,fontfile[2],fontfeature,range,scale,check,force,offset)
667 -- end
668    ctx_definefontfallback( { fontfallback }, { formatters["file:%s*%s"](fontfile[2],fontfeature) }, { range }, { rscale = scale, check = check, force = force, offset = offset } )
669end
670
671function selectfont.mathfallback(index,entry,class,style)
672    local data = data[entry]
673    ctx_startfontclass( { class } )
674        for alternative, _ in next, alternatives do
675            if alternative == "tf" or alternative == "bf" then
676                selectfont.filefallback(data,class,style,alternative,index)
677            end
678        end
679    ctx_stopfontclass()
680end
681
682function selectfont.textfallback(index,entry,class,style)
683    local data = data[entry]
684    ctx_startfontclass( { class } )
685        for alternative, _ in next, alternatives do
686            selectfont.fontsynonym (data,class,style,alternative,index)
687            selectfont.fontfallback(data,class,style,alternative,index)
688        end
689    ctx_stopfontclass()
690end
691
692function selectfont.fallback(data)
693    local fontclass = data.metadata.typeface
694    local fontstyle = data.metadata.style
695    local fallbacks = fallbacks[fontclass] and fallbacks[fontclass][fontstyle]
696    if fallbacks then
697        for index, entry in next, fallbacks do
698         -- I need different fallback routines for math and text because
699         -- font synonyms can’t be used with math fonts and I have to apply
700         -- feature settings with the \definefontfallback command.
701            if fontstyle == "mm" then
702                selectfont.mathfallback(index,entry,fontclass,fontstyle)
703            else
704                selectfont.textfallback(index,entry,fontclass,fontstyle)
705            end
706        end
707    end
708end
709
710function selectfont.typescript(data)
711    local class    = data.metadata.typeface
712    local family   = data.metadata.family
713    local style    = data.metadata.style
714    local extras   = data.options.extras
715    local defaults = defaults[cleanname(family)]
716    if extras == "" then
717        extras = defaults and defaults.options and defaults.options.extras or ""
718    end
719    ctx_startfontclass( { class } )
720        if extras ~= "" then
721            extras = settings_to_array(extras)
722            for _, extra in next, extras do
723                ctx_loadfontgoodies( { extra } )
724            end
725        end
726        for alternative, _ in next, alternatives do
727            if style == "mm" then
728             -- Set math fonts only for upright and bold alternatives
729                if alternative == "tf" or alternative == "bf" then
730                    selectfont.fontsynonym (data,class,style,alternative,0)
731                end
732            else
733                selectfont.fontsynonym (data,class,style,alternative,0)
734            end
735        end
736    ctx_stopfontclass()
737end
738
739function selectfont.bodyfont(data)
740    local class       = data.metadata.typeface
741    local fontstyle   = data.metadata.style
742    local fontclass   = lower(class)
743    local fontsizes   = concat(sortedkeys(bodyfontsizes),",")
744    local fontsynonym = nil
745    local fontlist    = { }
746    for alternative, _ in next, alternatives do
747        fontsynonym           = formatters["synonym-%s-%s-%s"](fontclass,fontstyle,alternative)
748        fontlist[#fontlist+1] = formatters["%s=%s sa 1"]      (alternative,fontsynonym)
749     -- if trace_typescript then
750     --     report_typescript("alternative '%s': synonym '%s'",alternative,fontsynonym)
751     -- end
752    end
753    fontlist = concat(fontlist,",")
754    ctx_definebodyfont( { class }, { fontsizes }, { fontstyle }, { fontlist } )
755end
756
757local m_style = {
758    ["rm"] = "serif",
759    ["ss"] = "sans",
760    ["tt"] = "mono",
761    ["mm"] = "math",
762    ["hw"] = "handwriting",
763    ["cg"] = "calligraphy",
764}
765
766function selectfont.typeface(data)
767    local fontclass = data.metadata.typeface
768    local fontstyle = data.metadata.style
769    local style     = m_style[fontstyle]
770    local size      = data.options.designsize ~= "" and data.options.designsize or "default"
771    local scale     = data.options.rscale     ~= "" and data.options.rscale     or 1
772 -- if trace_typescript then
773 --     report_typescript("class: '%s', style: '%s', size: '%s', scale: '%s'",fontclass,fontstyle,size,scale)
774 -- end
775    ctx_definetypeface( { fontclass }, { fontstyle }, { style }, { "" }, { "default" }, { designsize = size, rscale = scale } )
776end
777
778function selectfont.default(data)
779    local family    = data.metadata.family
780    local fontclass = data.metadata.typeface
781    local fontstyle = data.metadata.style
782    local style     = m_style[fontstyle]
783    report_selectfont("the requested font '%s' has no files for the 'tf' alternative, Latin Modern is used instead.",family)
784    ctx_definetypeface( { fontclass }, { fontstyle }, { style }, { "modern" }, { "default" } )
785end
786
787function selectfont.definefontfamily(index)
788    local data      = data[index]
789    local fontstyle = data.metadata.style
790    local fontfiles = data.files and data.files["tf"]
791    if fontfiles then
792        selectfont.fallback  (data)
793        selectfont.typescript(data)
794        if fontstyle ~= "mm" then
795            selectfont.bodyfont(data)
796        end
797        selectfont.typeface(data)
798    else
799        selectfont.default(data)
800    end
801end
802
803function selectfont.definefallbackfamily(index)
804    local data      = data[index]
805    local family    = data.metadata.family
806    local fontclass = data.metadata.typeface
807    local fontstyle = data.metadata.style
808    local fontfiles = data.files
809    if fontfiles then
810        selectfont.registerfallback(index)
811    else
812        report_selectfont("the requested fallback font '%s' for typeface '%s' style '%s' was ignored because no files where found.",family,fontclass,fontstyle)
813    end
814end
815
816function selectfont.definefontfamilypreset(name,data)
817    local p = presets[name]
818    local d = settings_to_hash(data)
819    if not p then
820        p = d
821        presets[name] = p
822    end
823end
824
825implement {
826    name      = "registerfontfamily",
827    actions   = { selectfont.registerfontfamily, context },
828    arguments = {
829        {
830            {
831                "metadata", {
832                    { "typeface" },
833                    { "style" },
834                    { "family" }
835                }
836            },
837            {
838                "options", {
839                    { "designsize" },
840                    { "rscale" },
841                    { "goodies" },
842                    { "preset" },
843                    { "extras" },
844                    { "features" },
845                    { "range" },
846                    { "offset" },
847                    { "check" },
848                    { "force" }
849                }
850            },
851            {
852                "userdata"
853            }
854        }
855    }
856}
857
858implement {
859    name      = "registerfontalternative",
860    actions   = selectfont.registerfontalternative,
861    arguments = "string"
862}
863
864implement {
865    name      = "definefontfamily",
866    actions   = selectfont.definefontfamily,
867    arguments = "integer"
868}
869
870implement {
871    name      = "definefallbackfamily",
872    actions   = selectfont.definefallbackfamily,
873    arguments = "integer"
874}
875
876implement {
877    name      = "definefontfamilypreset",
878    actions   = selectfont.definefontfamilypreset,
879    arguments = "2 strings",
880}
881