strc-bkm.lua /size: 17 Kb    last modification: 2020-07-01 14:35
1if not modules then modules = { } end modules ['strc-bkm'] = {
2    version   = 0.200,
3    comment   = "companion to strc-bkm.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- Future version will support adding arbitrary bookmarks with
10-- associated complex actions (rather trivial to implement).
11
12-- this should become proper separated backend code
13
14-- we should hook the placement into everystoptext ... needs checking
15
16-- todo: make an lpeg for stripped
17
18local next, type = next, type
19local gsub, lower = string.gsub, string.lower
20local concat = table.concat
21local settings_to_hash = utilities.parsers.settings_to_hash
22
23local trace_bookmarks  = false  trackers.register("references.bookmarks", function(v) trace_bookmarks = v end)
24local report_bookmarks = logs.reporter("structure","bookmarks")
25
26local structures     = structures
27
28structures.bookmarks = structures.bookmarks or { }
29
30local bookmarks      = structures.bookmarks
31local sections       = structures.sections
32local lists          = structures.lists
33local levelmap       = sections.levelmap
34local variables      = interfaces.variables
35local implement      = interfaces.implement
36local codeinjections = backends.codeinjections
37
38bookmarks.method     = "internal" -- or "page"
39
40local names          = { }
41local opened         = { }
42local forced         = { }
43local numbered       = { }
44
45function bookmarks.setopened(key,value)
46    if value == nil then
47        value = true
48    end
49    if type(key) == "table" then
50        for i=1,#key do
51            opened[key[i]] = value
52        end
53    else
54        opened[key] = value
55    end
56end
57
58function bookmarks.register(settings)
59    local force = settings.force == variables.yes
60    local number = settings.number == variables.yes
61    local allopen = settings.opened == variables.all
62    for k, v in next, settings_to_hash(settings.names or "") do
63        names[k] = true
64        if force then
65            forced[k] = true
66            if allopen then
67                opened[k] = true
68            end
69        end
70        if number then
71            numbered[k] = true
72        end
73    end
74    if not allopen then
75        for k, v in next, settings_to_hash(settings.opened or "") do
76            opened[k] = true
77        end
78    end
79end
80
81function bookmarks.overload(name,text)
82    local l, ls = lists.tobesaved, nil
83    if #l == 0 then
84        -- no entries
85    elseif name == "" then
86        ls = l[#l]
87    else
88        for i=#l,0,-1 do
89            local li = l[i]
90            local metadata = li.metadata
91            if metadata and not metadata.nolist and metadata.name == name then
92                ls = li
93                break
94            end
95        end
96    end
97    if ls then
98        local titledata = ls.titledata
99        if titledata then
100            titledata.bookmark = text
101        end
102    end
103    -- last resort
104 -- context.writetolist({name},text,"")
105end
106
107local function stripped(str) -- kind of generic
108    str = gsub(str,"\\([A-Z]+)","%1")            -- \LOGO
109    str = gsub(str,"\\ "," ")                    -- \
110    str = gsub(str,"\\([A-Za-z]+) *{(.-)}","%2") -- \bla{...}
111    str = gsub(str," +"," ")                     -- spaces
112    return str
113end
114
115-- todo: collect specs and collect later i.e. multiple places
116
117local numberspec = { }
118
119function bookmarks.setup(spec)
120 -- table.merge(numberspec,spec)
121    for k, v in next, spec do
122        numberspec[k] = v
123    end
124end
125
126function bookmarks.place()
127    if next(names) then
128        local levels         = { }
129        local noflevels      = 0
130        local lastlevel      = 1
131        local nofblocks      = #lists.sectionblocks -- always >= 1
132        local showblocktitle = toboolean(numberspec.showblocktitle,true)
133--         local allsections    = sections.collected
134        local allblocks      = sections.sectionblockdata
135        for i=1,nofblocks do
136            local block     = lists.sectionblocks[i]
137            local blockdone = nofblocks == 1
138            local list      = lists.filter {
139                names     = names,
140                criterium = block .. ":all",
141                forced    = forced,
142            }
143            for i=1,#list do
144                local li = list[i]
145                local metadata = li.metadata
146                local name = metadata.name
147                if not metadata.nolist or forced[name] then -- and levelmap[name] then
148                    local titledata = li.titledata
149                    --
150                    if not titledata then
151                        local userdata = li.userdata
152                        if userdata then
153                            local first  = userdata.first
154                            local second = userdata.second
155                            if first then
156                                if second then
157                                    titledata = { title = first .. " " .. second }
158                                else
159                                    titledata = { title = first }
160                                end
161                            elseif second then
162                                titledata = { title = second }
163                            else
164                                -- ignoring (command and so)
165                            end
166                        end
167                    end
168                    --
169                    if titledata then
170                        if not blockdone then
171                            if showblocktitle then
172                                -- add block entry
173                                local blockdata  = allblocks[block]
174                                local references = li.references
175                                noflevels = noflevels + 1
176                                levels[noflevels] = {
177                                    level     = 1, -- toplevel
178                                    title     = stripped(blockdata.bookmark ~= "" and blockdata.bookmark or block),
179                                    reference = references,
180                                    opened    = allopen or opened[name], -- same as first entry
181                                    realpage  = references and references.realpage or 0, -- handy for later
182                                    usedpage  = true,
183                                }
184                            end
185                            blockdone = true
186                        end
187                        local structural = levelmap[name]
188                        lastlevel = structural or lastlevel
189                        if nofblocks > 1 then
190                            -- we have a block so increase the level
191                            lastlevel = lastlevel + 1
192                        end
193                        local title = titledata.bookmark
194                        if not title or title == "" then
195                            -- We could typeset the title and then convert it.
196                         -- if not structural then
197                         --     title = titledata.title or "?")
198                         -- else
199                                title = titledata.title or "?"
200                         -- end
201                        end
202--                         if numbered[name] then
203--                             local sectiondata = allsections[li.references.section]
204--                             if sectiondata then
205--                                 local numberdata = li.numberdata
206--                                 if numberdata and not numberdata.hidenumber then
207--                                  -- we could typeset the number and convert it
208--                                     local number = sections.typesetnumber(sectiondata,"direct",numberspec,sectiondata)
209--                                     if number and #number > 0 then
210--                                         title = concat(number) .. " " .. title
211--                                     end
212--                                 end
213--                             end
214--                         end
215if numbered[name] then
216    local numberdata = li.numberdata
217    if numberdata and not numberdata.hidenumber then
218     -- we could typeset the number and convert it
219        local number = sections.typesetnumber(numberdata,"direct",numberspec,numberdata)
220        if number and #number > 0 then
221            title = concat(number) .. " " .. title
222        end
223    end
224end
225                        noflevels = noflevels + 1
226                        local references = li.references
227                        levels[noflevels] = {
228                            level      = lastlevel,
229                            title      = stripped(title), -- can be replaced by converter
230                            reference  = references,   -- has internal and realpage
231                            opened     = allopen or opened[name],
232                            realpage   = references and references.realpage or 0, -- handy for later
233                            usedpage   = true,
234                            structural = structural,
235                            name       = name,
236                        }
237                    end
238                end
239            end
240        end
241-- inspect(levels)
242        bookmarks.finalize(levels)
243        function bookmarks.place() end -- prevent second run
244    end
245end
246
247function bookmarks.flatten(levels)
248    if not levels then
249        -- a plugin messed up
250        return { }
251    end
252    -- This function promotes leading structurelements with a higher level
253    -- to the next lower level. Such situations are the result of lack of
254    -- structure: a subject preceding a chapter in a sectionblock. So, the
255    -- following code runs over section blocks as well. (bookmarks-007.tex)
256    local noflevels = #levels
257    if noflevels > 1 then
258        local function showthem()
259            for i=1,noflevels do
260                local level = levels[i]
261             -- if level.structural then
262             --     report_bookmarks("%i > %s > %s",level.level,level.reference.block,level.title)
263             -- else
264                    report_bookmarks("%i > %s > %s > %s",level.level,level.reference.block,level.name,level.title)
265             -- end
266            end
267        end
268        if trace_bookmarks then
269            report_bookmarks("checking structure")
270            showthem()
271        end
272        local skip  = false
273        local done  = 0
274        local start = 1
275        local one   = levels[1]
276        local first = one.level
277        local block = one.reference.block
278        for i=2,noflevels do
279            local current   = levels[i]
280            local new       = current.level
281            local reference = current.reference
282            local newblock  = type(reference) == "table" and current.reference.block or block
283            if newblock ~= block then
284                first = new
285                block = newblock
286                start = i
287                skip  = false
288            elseif skip then
289                -- go on
290            elseif new > first then
291                skip = true
292            elseif new < first then
293                for j=start,i-1 do
294                    local previous = levels[j]
295                    local old      = previous.level
296                    previous.level = new
297                    if trace_bookmarks then
298                        report_bookmarks("promoting entry %a from level %a to %a: %s",j,old,new,previous.title)
299                    end
300                    done = done + 1
301                end
302                skip = true
303            end
304        end
305        if trace_bookmarks then
306            if done > 0 then
307                report_bookmarks("%a entries promoted")
308                showthem()
309            else
310                report_bookmarks("nothing promoted")
311            end
312        end
313    end
314    return levels
315end
316
317local extras = { }
318local lists  = { }
319local names  = { }
320
321bookmarks.extras = extras
322
323local function cleanname(name)
324    return lower(file.basename(name))
325end
326
327function extras.register(name,levels)
328    if name and levels then
329        name = cleanname(name)
330        local found = names[name]
331        if found then
332            lists[found].levels = levels
333        else
334            lists[#lists+1] = {
335                name   = name,
336                levels = levels,
337            }
338            names[name] = #lists
339        end
340    end
341end
342
343function extras.get(name)
344    if name then
345        local found = names[cleanname(name)]
346        if found then
347            return lists[found].levels
348        end
349    else
350        return lists
351    end
352end
353
354function extras.reset(name)
355    local l, n = { }, { }
356    if name then
357        name = cleanname(name)
358        for i=1,#lists do
359            local li = lists[i]
360            local ln = li.name
361            if name == ln then
362                -- skip
363            else
364                local m = #l + 1
365                l[m]  = li
366                n[ln] = m
367            end
368        end
369    end
370    lists, names = l, n
371end
372
373local function checklists()
374    for i=1,#lists do
375        local levels = lists[i].levels
376        for j=1,#levels do
377            local entry     = levels[j]
378            local pageindex = entry.pageindex
379            if pageindex then
380                entry.reference = figures.getrealpage(pageindex)
381                entry.pageindex = nil
382            end
383        end
384    end
385end
386
387function extras.tosections(levels)
388    local sections = { }
389    local noflists = #lists
390    for i=1,noflists do
391        local levels = lists[i].levels
392        local data   = { }
393        sections[i]  = data
394        for j=1,#levels do
395            local entry = levels[j]
396            if entry.usedpage then
397                local section = entry.section
398                local d = data[section]
399                if d then
400                    d[#d+1] = entry
401                else
402                    data[section] = { entry }
403                end
404            end
405        end
406    end
407    return sections
408end
409
410function extras.mergesections(levels,sections)
411    if not sections or #sections == 0 then
412        return levels
413    elseif not levels then
414        return { }
415    else
416        local merge    = { }
417        local noflists = #lists
418        if #levels == 0 then
419            local level   = 0
420            local section = 0
421            for i=1,noflists do
422                local entries = sections[i][0]
423                if entries then
424                    for i=1,#entries do
425                        local entry = entries[i]
426                        merge[#merge+1] = entry
427                        entry.level = entry.level + level
428                    end
429                end
430            end
431        else
432            for j=1,#levels do
433                local entry     = levels[j]
434                merge[#merge+1] = entry
435                local section   = entry.reference.section
436                local level     = entry.level
437                entry.section   = section -- for tracing
438                for i=1,noflists do
439                    local entries = sections[i][section]
440                    if entries then
441                        for i=1,#entries do
442                            local entry = entries[i]
443                            merge[#merge+1] = entry
444                            entry.level = entry.level + level
445                        end
446                    end
447                end
448            end
449        end
450        return merge
451    end
452end
453
454function bookmarks.merge(levels,mode)
455    return extras.mergesections(levels,extras.tosections())
456end
457
458local sequencers   = utilities.sequencers
459local appendgroup  = sequencers.appendgroup
460local appendaction = sequencers.appendaction
461
462local bookmarkactions = sequencers.new {
463    arguments    = "levels,method",
464    returnvalues = "levels",
465    results      = "levels",
466}
467
468appendgroup(bookmarkactions,"before") -- user
469appendgroup(bookmarkactions,"system") -- private
470appendgroup(bookmarkactions,"after" ) -- user
471
472appendaction(bookmarkactions,"system",bookmarks.flatten)
473appendaction(bookmarkactions,"system",bookmarks.merge)
474
475function bookmarks.finalize(levels)
476    local method = bookmarks.method or "internal"
477    checklists() -- so that plugins have the adapted page number
478    levels = bookmarkactions.runner(levels,method)
479    if levels and #levels > 0 then
480        -- normally this is not needed
481        local purged = { }
482        for i=1,#levels do
483            local l = levels[i]
484            if l.usedpage ~= false then
485                purged[#purged+1] = l
486            end
487        end
488        --
489        codeinjections.addbookmarks(purged,method)
490    else
491        -- maybe a plugin messed up
492    end
493end
494
495function bookmarks.installhandler(what,where,func)
496    if not func then
497        where, func = "after", where
498    end
499    if where == "before" or where == "after" then
500        sequencers.appendaction(bookmarkactions,where,func)
501    else
502        report_tex("installing bookmark %a handlers in %a is not possible",what,tostring(where))
503    end
504end
505
506-- interface
507
508implement {
509    name      = "setupbookmarks",
510    actions   = bookmarks.setup,
511    arguments = {
512        {
513            { "separatorset" },
514            { "conversionset" },
515            { "starter" },
516            { "stopper" },
517            { "segments" },
518            { "showblocktitle" },
519        }
520    }
521}
522
523implement {
524    name      = "registerbookmark",
525    actions   = bookmarks.register,
526    arguments = {
527        {
528            { "names" },
529            { "opened" },
530            { "force" },
531            { "number" },
532        }
533    }
534}
535
536implement {
537    name      = "overloadbookmark",
538    actions   = bookmarks.overload,
539    arguments = "2 strings",
540}
541