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