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