1if not modules then modules = { } end modules ['scrp-ini'] = {
2 version = 1.001,
3 comment = "companion to scrp-ini.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
10
11
12local tonumber, next = tonumber, next
13local setmetatableindex = table.setmetatableindex
14local utfbyte, utfsplit = utf.byte, utf.split
15local gmatch = string.gmatch
16
17local trace_analyzing = false trackers.register("scripts.analyzing", function(v) trace_analyzing = v end)
18local trace_injections = false trackers.register("scripts.injections", function(v) trace_injections = v end)
19local trace_splitting = false trackers.register("scripts.splitting", function(v) trace_splitting = v end)
20local trace_splitdetails = false trackers.register("scripts.splitting.details", function(v) trace_splitdetails = v end)
21
22local report_preprocessing = logs.reporter("scripts","preprocessing")
23local report_splitting = logs.reporter("scripts","splitting")
24
25local attributes = attributes
26local nodes = nodes
27local context = context
28
29local implement = interfaces.implement
30
31local glyph_code <const> = nodes.nodecodes.glyph
32local glue_code <const> = nodes.nodecodes.glue
33
34local emwidths = fonts.hashes.emwidths
35local exheights = fonts.hashes.exheights
36
37local a_script <const> = attributes.private('script')
38
39local fontdata = fonts.hashes.identifiers
40local allocate = utilities.storage.allocate
41local setnodecolor = nodes.tracers.colors.set
42
43local enableaction = nodes.tasks.enableaction
44local disableaction = nodes.tasks.disableaction
45
46local nuts = nodes.nuts
47
48local getnext = nuts.getnext
49local getchar = nuts.getchar
50local getfont = nuts.getfont
51local getscript = nuts.getscript
52local getid = nuts.getid
53local getglyphdata = nuts.getglyphdata
54local setglyphdata = nuts.setglyphdata
55
56local isglyph = nuts.isglyph
57
58local firstglyphnode = nuts.firstglyphnode
59
60local nextglyph = nuts.traversers.glyph
61local nextchar = nuts.traversers.char
62
63local texsetglyphscript = tex.setglyphscript
64
65local nodepool = nuts.pool
66
67local new_glue = nodepool.glue
68local new_rule = nodepool.rule
69local new_penalty = nodepool.penalty
70
71scripts = scripts or { }
72local scripts = scripts
73
74local handlers = allocate()
75scripts.handlers = handlers
76
77local injectors = allocate()
78scripts.injectors = handlers
79
80local splitters = allocate()
81scripts.splitters = splitters
82
83local helpers = allocate()
84scripts.helpers = helpers
85
86local insertnodebefore, insertnodeafter do
87
88 local insertafter = nuts.insertafter
89 local insertbefore = nuts.insertbefore
90 local setattributelist = nuts.setattributelist
91
92 insertnodebefore = function (head,current,what)
93 setattributelist(what,current)
94 head, current = insertbefore(head,current,what)
95 return head, current
96 end
97
98 insertnodeafter = function(head,current,what)
99 setattributelist(what,current)
100 head, current = insertafter(head,current,what)
101 return head, current
102 end
103
104 helpers.insertnodebefore = insertnodebefore
105 helpers.insertnodeafter = insertnodeafter
106
107end
108
109local hash = characters.scripthash
110
111local numbertodataset = allocate()
112local numbertohandler = allocate()
113
114scripts.numbertodataset = numbertodataset
115scripts.numbertohandler = numbertohandler
116
117local defaults = {
118 inter_char_shrink_factor = 0,
119 inter_char_shrink_factor = 0,
120 inter_char_stretch_factor = 0,
121 inter_char_half_shrink_factor = 0,
122 inter_char_half_stretch_factor = 0,
123 inter_char_quarter_shrink_factor = 0,
124 inter_char_quarter_stretch_factor = 0,
125 inter_char_hangul_penalty = 0,
126
127 inter_word_stretch_factor = 0,
128}
129
130scripts.defaults = defaults
131
132
133
134function scripts.installmethod(handler)
135 local name = handler.name
136 handlers[name] = handler
137 local attributes = { }
138 local datasets = handler.datasets
139 if not datasets or not datasets.default then
140 report_preprocessing("missing (default) dataset in script %a",name)
141 datasets.default = { }
142 end
143
144 for k, v in next, datasets do
145 setmetatableindex(v,defaults)
146 end
147 setmetatableindex(attributes, function(t,k)
148 local v = datasets[k] or datasets.default
149 local a = 0
150 if v then
151 v.name = name
152 a = #numbertodataset + 1
153 numbertodataset[a] = v
154 numbertohandler[a] = handler
155 end
156 t[k] = a
157 return a
158 end)
159 handler.attributes = attributes
160end
161
162function scripts.installdataset(specification)
163 local method = specification.method
164 local name = specification.name
165 local dataset = specification.dataset
166 if method and name and dataset then
167 local parent = specification.parent or ""
168 local handler = handlers[method]
169 if handler then
170 local datasets = handler.datasets
171 if datasets then
172 local defaultset = datasets.default
173 if defaultset then
174 if parent ~= "" then
175 local p = datasets[parent]
176 if p then
177 defaultset = p
178 else
179 report_preprocessing("dataset, unknown parent %a for method %a",parent,method)
180 end
181 end
182 setmetatable(dataset,defaultset)
183 local existing = datasets[name]
184 if existing then
185 for k, v in next, existing do
186 existing[k] = dataset
187 end
188 else
189 datasets[name] = dataset
190 end
191 else
192 report_preprocessing("dataset, no default for method %a",method)
193 end
194 else
195 report_preprocessing("dataset, no datasets for method %a",method)
196 end
197 else
198 report_preprocessing("dataset, no method %a",method)
199 end
200 else
201 report_preprocessing("dataset, invalid specification")
202 end
203end
204
205local injectorenabled = false
206local splitterenabled = false
207
208local function getscriptdata(n)
209 local s = getscript(n)
210 if s then
211 return s and numbertodataset[s]
212 end
213end
214
215local function getinjector(n)
216 local s = getscript(n)
217 if s then
218 s = numbertohandler[s]
219 return s and s.injector
220 end
221end
222
223local function getsplitter(n)
224 local s = getscript(n)
225 if s then
226 s = numbertodataset[s]
227 return s and s.splitter
228 end
229end
230
231scripts.getdata = getscriptdata
232scripts.getinjector = getinjector
233scripts.getsplitter = getsplitter
234
235function scripts.set(name,method,preset)
236 local handler = handlers[method]
237 if handler then
238 local index = handler.attributes[preset]
239 if handler.injector then
240 if not injectorenabled then
241 enableaction("processors","scripts.injectors.handler")
242 injectorenabled = true
243 end
244 end
245 if handler.splitter then
246 if not splitterenabled then
247 enableaction("processors","scripts.splitters.handler")
248 splitterenabled = true
249 end
250 end
251 if handler.initializer then
252 handler.initializer(handler)
253 handler.initializer = nil
254 end
255 texsetglyphscript(index)
256 else
257 texsetglyphscript()
258 end
259end
260
261function scripts.reset()
262 texsetglyphscript()
263end
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291local scriptcolors = allocate {
292
293 hyphen = "trace:5",
294}
295
296scripts.colors = scriptcolors
297
298
299
300local propertydata = nodes.properties.data
301
302local function setscriptstatus(n,s)
303 local p = propertydata[n]
304 if p then
305 p.scriptstatus = s
306 else
307 propertydata[n] = { scriptstatus = s }
308 end
309end
310
311function getscriptstatus(n)
312 local p = propertydata[n]
313 if p then
314 return p.scriptstatus
315 end
316end
317
318scripts.setstatus = setscriptstatus
319scripts.getstatus = getscriptstatus
320
321
322
323local function colorize(start,stop)
324 for n in nextglyph, start do
325 local kind = getscriptstatus(n)
326 if kind then
327 local ac = scriptcolors[kind]
328 if ac then
329 setnodecolor(n,ac)
330 end
331 end
332 if n == stop then
333 break
334 end
335 end
336end
337
338local function traced_process(head,first,last,process,a)
339 if start ~= last then
340 local f, l = first, last
341 local name = numbertodataset[a]
342 name = name and name.name or "?"
343 report_preprocessing("before %s: %s",name,nodes.tosequence(f,l))
344 process(head,first,last)
345 report_preprocessing("after %s: %s", name,nodes.tosequence(f,l))
346 end
347end
348
349function scripts.injectors.handler(head)
350 local start = firstglyphnode(head)
351 if not start then
352 return head
353 else
354 local last_a, normal_process, lastfont, originals, first, last
355 local ok = false
356 while start do
357 local char, id = isglyph(start)
358 if char then
359 local a = getscript(start)
360 if a then
361 if a ~= last_a then
362 if first then
363 if ok then
364 if trace_analyzing then
365 colorize(first,last)
366 end
367 if trace_injections then
368 traced_process(head,first,last,normal_process,last_a)
369 else
370 normal_process(head,first,last)
371 end
372 ok = false
373 end
374 first, last = nil, nil
375 end
376 last_a = a
377
378 normal_process = numbertohandler[a]
379 if normal_process then
380 normal_process = normal_process.injector
381 end
382 end
383 if normal_process then
384 if id ~= lastfont then
385 originals = fontdata[id].resources
386 if resources then
387 originals = resources.originals
388 else
389 originals = nil
390 end
391 lastfont = id
392 end
393 if originals and type(originals) == "number" then
394 char = originals[char] or char
395 end
396 local h = hash[char]
397 if h then
398 setscriptstatus(start,h)
399 if not first then
400 first, last = start, start
401 else
402 last = start
403 end
404
405 ok = true
406
407 elseif first then
408 if ok then
409 if trace_analyzing then
410 colorize(first,last)
411 end
412 if trace_injections then
413 traced_process(head,first,last,normal_process,last_a)
414 else
415 normal_process(head,first,last)
416 end
417 ok = false
418 end
419 first, last = nil, nil
420 end
421 end
422 elseif first then
423 if ok then
424 if trace_analyzing then
425 colorize(first,last)
426 end
427 if trace_injections then
428 traced_process(head,first,last,normal_process,last_a)
429 else
430 normal_process(head,first,last)
431 end
432 ok = false
433 end
434 first, last = nil, nil
435 end
436 elseif id == glue_code then
437 if ok then
438
439 elseif first then
440
441 first, last = nil, nil
442 end
443 elseif first then
444 if ok then
445
446 if trace_analyzing then
447 colorize(first,last)
448 end
449 if trace_injections then
450 traced_process(head,first,last,normal_process,last_a)
451 else
452 normal_process(head,first,last)
453 end
454 first, last, ok = nil, nil, false
455 elseif first then
456 first, last = nil, nil
457 end
458 end
459 start = getnext(start)
460 end
461 if ok then
462 if trace_analyzing then
463 colorize(first,last)
464 end
465 if trace_injections then
466 traced_process(head,first,last,normal_process,last_a)
467 else
468 normal_process(head,first,last)
469 end
470 end
471 return head
472 end
473end
474
475
476
477
478
479
480
481local function addwords(tree,data)
482 if not tree then
483 tree = { }
484 end
485 for word in gmatch(data,"%S+") do
486 local root = tree
487 local list = utfsplit(word,true)
488 for i=1,#list do
489 local l = utfbyte(list[i])
490 local r = root[l]
491 if not r then
492 r = { }
493 root[l] = r
494 end
495 if i == #list then
496 r.final = word
497 else
498 root = r
499 end
500 end
501 end
502 return tree
503end
504
505local loaded = { }
506
507function splitters.load(handler,files)
508 local files = handler.files
509 local tree = handler.tree or { }
510 handler.tree = tree
511 if not files then
512 return
513 elseif type(files) == "string" then
514 files = { files }
515 handler.files = files
516 end
517 if trace_splitting then
518 report_splitting("loading splitter data for language/script %a",handler.name)
519 end
520 loaded[handler.name or "unknown"] = (loaded[handler.name or "unknown"] or 0) + 1
521 statistics.starttiming(loaded)
522 for i=1,#files do
523 local filename = files[i]
524 local fullname = resolvers.findfile(filename)
525 if fullname == "" then
526 fullname = resolvers.findfile(filename .. ".gz")
527 end
528 if fullname ~= "" then
529 if trace_splitting then
530 report_splitting("loading file %a",fullname)
531 end
532 local suffix, gzipped = gzip.suffix(fullname)
533 if suffix == "lua" then
534 local specification = table.load(fullname,gzipped and gzip.load)
535 if specification then
536 local lists = specification.lists
537 if lists then
538 for i=1,#lists do
539 local entry = lists[i]
540 local data = entry.data
541 if data then
542 if entry.compression == "zlib" then
543 data = zlib.decompress(data)
544 if entry.length and entry.length ~= #data then
545 report_splitting("compression error in file %a",fullname)
546 end
547 end
548 if data then
549 addwords(tree,data)
550 end
551 end
552 end
553 end
554 end
555 else
556 local data = gzipped and io.loadgzip(fullname) or io.loaddata(fullname)
557 if data then
558 addwords(tree,data)
559 end
560 end
561 else
562 report_splitting("unknown file %a",filename)
563 end
564 end
565 statistics.stoptiming(loaded)
566 return tree
567end
568
569statistics.register("loaded split lists", function()
570 if next(loaded) then
571 return string.format("%s, load time: %s",table.sequenced(loaded),statistics.elapsedtime(loaded))
572 end
573end)
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598local categories = characters.categories or { }
599
600local function hit(root,head)
601 local current = getnext(head)
602 local lastrun = false
603 local lastfinal = false
604 while current do
605 local char = isglyph(current)
606 if char then
607 local newroot = root[char]
608 if newroot then
609 local final = newroot.final
610 if final then
611 lastrun = current
612 lastfinal = final
613 end
614 root = newroot
615 elseif categories[char] == "mn" then
616
617 else
618 return lastrun, lastfinal
619 end
620 else
621 break
622 end
623 end
624 if lastrun then
625 return lastrun, lastfinal
626 end
627end
628
629local tree, attr, proc
630
631function splitters.handler(head)
632 local current = head
633 while current do
634 if getid(current) == glyph_code then
635 local a = getsplitter(current)
636 if a then
637 if a ~= attr then
638 local handler = numbertohandler[a]
639 tree = handler.tree or { }
640 attr = a
641 proc = a
642 end
643 if proc then
644 local root = tree[getchar(current)]
645 if root then
646
647 local last, final = hit(root,current)
648 if last then
649 local next = getnext(last)
650 if next then
651 local nextchar = isglyph(next)
652 if not nextchar then
653
654 elseif tree[nextchar] then
655 if trace_splitdetails then
656 if type(final) == "string" then
657 report_splitting("advance %s processing between <%s> and <%c>","with",final,nextchar)
658 else
659 report_splitting("advance %s processing between <%c> and <%c>","with",char,nextchar)
660 end
661 end
662 head, current = proc(handler,head,current,last,1)
663 else
664 if trace_splitdetails then
665
666 if type(final) == "string" then
667 report_splitting("advance %s processing between <%s> and <%c>","without",final,nextchar)
668 else
669 report_splitting("advance %s processing between <%c> and <%c>","without",char,nextchar)
670 end
671 end
672 head, current = proc(handler,head,current,last,2)
673 end
674 end
675 end
676 end
677 end
678 end
679 end
680 current = getnext(current)
681 end
682 return head
683end
684
685local function marker(head,current,font,color)
686 local ex = exheights[font]
687 local em = emwidths [font]
688 head, current = insertnodeafter(head,current,new_penalty(10000))
689 head, current = insertnodeafter(head,current,new_glue(-0.05*em))
690 head, current = insertnodeafter(head,current,new_rule(0.05*em,1.5*ex,0.5*ex))
691 setnodecolor(current,color)
692 return head, current
693end
694
695local last_a, last_f, last_s, last_q
696
697function splitters.insertafter(handler,head,first,last,detail)
698 local a = getscriptdata(first)
699 local f = getfont(first)
700 if a and a ~= last_a or f ~= last_f then
701 last_s = emwidths[f] * data.inter_word_stretch_factor
702 last_a = a
703 last_f = f
704 end
705 if trace_splitting then
706 head, last = marker(head,last,f,detail == 2 and "trace:r" or "trace:g")
707 end
708 if ignore then
709 return head, last
710 else
711 return insertnodeafter(head,last,new_glue(0,last_s))
712 end
713end
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731scripts.installmethod {
732 name = "test",
733 splitter = splitters.insertafter,
734 initializer = splitters.load,
735 files = {
736
737 "word-xx.lua",
738 },
739 datasets = {
740 default = {
741 inter_word_stretch_factor = 0.25,
742 },
743 },
744}
745
746
747
748local registercontext = fonts.specifiers.registercontext
749local mergecontext = fonts.specifiers.mergecontext
750
751local otfscripts = characters.otfscripts
752
753local report_scripts = logs.reporter("scripts","auto feature")
754local trace_scripts = false trackers.register("scripts.autofeature",function(v) trace_scripts = v end)
755
756local autofontfeature = scripts.autofontfeature or { }
757scripts.autofontfeature = autofontfeature
758
759local cache_yes = { }
760local cache_nop = { }
761
762setmetatableindex(cache_yes,function(t,k) local v = { } t[k] = v return v end)
763setmetatableindex(cache_nop,function(t,k) local v = { } t[k] = v return v end)
764
765
766
767
768
769
770
771
772
773
774
775function autofontfeature.handler(head)
776 for n, char, font in nextchar, head do
777
778
779
780 local script = otfscripts[char]
781 if script then
782 local dynamic = getglyphdata(n) or 0
783 if dynamic > 0 then
784 local slot = cache_yes[font]
785 local attr = slot[script]
786 if not attr then
787 attr = mergecontext(dynamic,name,2)
788 slot[script] = attr
789 if trace_scripts then
790 report_scripts("script: %s, trigger %C, dynamic: %a, variant: %a",script,char,attr,"extended")
791 end
792 end
793 if attr ~= 0 then
794 n[0] = attr
795
796 end
797 else
798 local slot = cache_nop[font]
799 local attr = slot[script]
800 if not attr then
801 attr = registercontext(font,script,2)
802 slot[script] = attr
803 if trace_scripts then
804 report_scripts("script: %s, trigger %C, dynamic: %s, variant: %a",script,char,attr,"normal")
805 end
806 end
807 if attr ~= 0 then
808 setglyphdata(n,attr)
809
810 end
811 end
812 end
813
814 end
815 return head
816end
817
818function autofontfeature.enable()
819 report_scripts("globally enabled")
820 enableaction("processors","scripts.autofontfeature.handler")
821end
822
823function autofontfeature.disable()
824 report_scripts("globally disabled")
825 disableaction("processors","scripts.autofontfeature.handler")
826end
827
828implement {
829 name = "enableautofontscript",
830 actions = autofontfeature.enable
831}
832
833implement {
834 name = "disableautofontscript",
835 actions = autofontfeature.disable }
836
837implement {
838 name = "setscript",
839 actions = scripts.set,
840 arguments = "3 strings",
841}
842
843implement {
844 name = "resetscript",
845 actions = scripts.reset
846}
847
848
849
850do
851
852 local parameters = fonts.hashes.parameters
853
854 local space, stretch, shrink, lastfont
855
856 local inter_character_space_factor = 1
857 local inter_character_stretch_factor = 1
858 local inter_character_shrink_factor = 1
859
860 local function space_glue(current)
861
862 local data = getscriptdata(current)
863 if data then
864 inter_character_space_factor = data.inter_character_space_factor or 1
865 inter_character_stretch_factor = data.inter_character_stretch_factor or 1
866 inter_character_shrink_factor = data.inter_character_shrink_factor or 1
867 end
868 local font = getfont(current)
869 if lastfont ~= font then
870 local pf = parameters[font]
871 space = pf.space
872 stretch = pf.spacestretch
873 shrink = pf.spaceshrink
874 lastfont = font
875 end
876 return new_glue(
877 inter_character_space_factor * space,
878 inter_character_stretch_factor * stretch,
879 inter_character_shrink_factor * shrink
880 )
881 end
882
883 scripts.inserters = {
884
885 space_before = function(head,current)
886 return insertnodebefore(head,current,space_glue(current))
887 end,
888 space_after = function(head,current)
889 return insertnodeafter(head,current,space_glue(current))
890 end,
891
892 zerowidthspace_before = function(head,current)
893 return insertnodebefore(head,current,new_glue(0))
894 end,
895 zerowidthspace_after = function(head,current)
896 return insertnodeafter(head,current,new_glue(0))
897 end,
898
899 nobreakspace_before = function(head,current)
900 local g = space_glue(current)
901 local p = new_penalty(10000)
902 head, current = insertnodebefore(head,current,p)
903 return insertnodebefore(head,current,g)
904 end,
905 nobreakspace_after = function(head,current)
906 local g = space_glue(current)
907 local p = new_penalty(10000)
908 head, current = insertnodeafter(head,current,g)
909 return insertnodeafter(head,current,p)
910 end,
911
912 }
913
914end
915
916
917 |