cbeta 3.8.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,751 +0,0 @@
1
- require 'cgi'
2
- require 'date'
3
- require 'fileutils'
4
- require 'json'
5
- require 'nokogiri'
6
- require 'set'
7
- require 'erb'
8
- require_relative 'cbeta_share'
9
-
10
- # Convert CBETA XML P5a to HTML for PDF
11
- #
12
- # You can get CBETA XML P5a from: https://github.com/cbeta-git/xml-p5a
13
- class CBETA::P5aToHTMLForPDF
14
- # 內容不輸出的元素
15
- PASS=['back', 'teiHeader']
16
-
17
- # 某版用字缺的符號
18
- MISSING = '-'
19
-
20
- private_constant :PASS, :MISSING
21
-
22
- # @param xml_root [String] 來源 CBETA XML P5a 路徑
23
- # @param out_root [String] 輸出 HTML 路徑
24
- # @option opts [String] :graphic_base folder of graphics
25
- # * graphic_base/figures: 插圖圖檔位置
26
- # * graphic_base/sd-gif: images for Siddham (悉曇字)
27
- # * graphic_base/rj-gif: images for Ranjana (蘭札體)
28
- # @option opts [String] :front_page 內文前可以加一段 HTML,例如「編輯說明」
29
- # @option opts [String] :front_page_title 加在目錄的 front_page 標題
30
- # @option opts [String] :back_page 內文後可以加一段 HTML,例如「版權聲明」
31
- # @option opts [String] :back_page_title 加在目錄的 back_page 標題
32
- # @option opts [Boolean] :toc 要不要放目次, 預設會有目次
33
- def initialize(xml_root, out_root, opts={})
34
- @config = {
35
- toc: true
36
- }
37
- @config.merge!(opts)
38
-
39
- @xml_root = xml_root
40
- @out_root = out_root
41
- @cbeta = CBETA.new
42
- @gaijis = CBETA::Gaiji.new
43
- end
44
-
45
- # 將 CBETA XML P5a 轉為 HTML 供轉為 PDF
46
- #
47
- # @example for convert 大正藏全部:
48
- #
49
- # c = CBETA::P5aToHTMLForPDF.new('/PATH/TO/CBETA/XML/P5a', '/OUTPUT/FOLDER')
50
- # c.convert('T')
51
- #
52
- # T 是大正藏的 ID, CBETA 的藏經 ID 系統請參考: http://www.cbeta.org/format/id.php
53
- def convert(target=nil)
54
- return convert_all if target.nil?
55
-
56
- arg = target.upcase
57
- convert_collection(arg)
58
- end
59
-
60
- private
61
-
62
- include CbetaShare
63
-
64
- def before_convert_work(work_id)
65
- @nav_doc = Nokogiri::XML('<ul></ul>')
66
- @nav_doc.remove_namespaces!()
67
- @nav_root = @nav_doc.at_xpath('/ul')
68
- @current_nav = [@nav_root]
69
- @div_count = 0
70
- @mulu_count = 0
71
- @open_divs = []
72
- @text = ''
73
-
74
- @output_folder_work = File.join(@out_root, @series, work_id)
75
- FileUtils.mkdir_p(@output_folder_work) unless Dir.exist? @output_folder_work
76
-
77
- src = File.join(CBETA::DATA, 'html-for-pdf.css')
78
- copy_file(src)
79
-
80
- @cover = nil
81
- if @config.key? :graphic_base
82
- cover = File.join(@config[:graphic_base], 'covers', @series, "#{work_id}.jpg")
83
- if File.exist? cover
84
- @mulu_count += 1
85
- @cover = "<a id='mulu#{@mulu_count}'></a><mulu1 title='封面'>&nbsp;</mulu1>"
86
- @cover += "<div id='cover'><img src='#{work_id}.jpg' /></div>"
87
- copy_file(cover)
88
- end
89
- end
90
-
91
- if @config[:front_page_title]
92
- s = @config[:front_page_title]
93
- @nav_root.add_child("<li><a href='#front'>#{s}</a></li>")
94
- end
95
-
96
- end
97
-
98
- def before_parse_xml(xml_fn)
99
- @in_l = false
100
- @lg_row_open = false
101
- @t_buf1 = []
102
- @t_buf2 = []
103
- @sutra_no = File.basename(xml_fn, ".xml")
104
- end
105
-
106
- def convert_all
107
- Dir.foreach(@xml_root) { |c|
108
- next unless c.match(/^[A-Z]$/)
109
- convert_collection(c)
110
- }
111
- end
112
-
113
- def convert_collection(c)
114
- @series = c
115
- puts 'convert_collection ' + c
116
-
117
- @orig = @cbeta.get_canon_abbr(c)
118
-
119
- folder = File.join(@xml_root, @series)
120
- @works = {}
121
- prepare_work_list(folder)
122
- @works.each do |work_id, xml_files|
123
- convert_work(work_id, xml_files)
124
- end
125
- end
126
-
127
- def convert_work(work_id, xml_files)
128
- puts "convert xml to html: work_id: #{work_id}"
129
-
130
- before_convert_work(work_id)
131
-
132
- # 目次
133
- if @config[:back_page_title]
134
- s = @config[:back_page_title]
135
- @nav_root.add_child("<li><a href='#back'>#{s}</a></li>")
136
- end
137
-
138
-
139
- if @config.key? :front_page
140
- s = File.read(@config[:front_page])
141
- @front = "<div id='front'>#{s}</div>"
142
- end
143
-
144
- if @config.key? :back_page
145
- s = File.read(@config[:back_page])
146
- @back = "<div id='back'>#{s}</div>"
147
- end
148
-
149
- xml_files.each do |fn|
150
- @text += convert_xml_file(fn)
151
- end
152
-
153
- if @config[:toc]
154
- @toc = to_html(@nav_root)
155
- @toc.gsub!('<ul/>', '')
156
- @toc = "<div><h1>目次</h1>#{@toc}</div>"
157
- else
158
- @toc = ''
159
- end
160
-
161
- fn = File.join(CBETA::DATA, 'pdf-template.htm')
162
- template = File.read(fn)
163
- output = template % {
164
- cover: @cover,
165
- toc: @toc,
166
- front: @front,
167
- text: @text,
168
- back: @back
169
- }
170
-
171
- fn = File.join(@output_folder_work, 'main.htm')
172
- File.write(fn, output)
173
- end
174
-
175
- def convert_xml_file(xml_fn)
176
- before_parse_xml(xml_fn)
177
- parse_xml(xml_fn)
178
- end
179
-
180
- def copy_file(src)
181
- basename = File.basename(src)
182
- dest = File.join(@output_folder_work, basename)
183
- FileUtils.copy(src, dest)
184
- end
185
-
186
- def handle_anchor(e)
187
- if e.has_attribute?('type')
188
- if e['type'] == 'circle'
189
- return '◎'
190
- end
191
- end
192
-
193
- ''
194
- end
195
-
196
- def handle_app(e)
197
- traverse(e)
198
- end
199
-
200
- def handle_byline(e)
201
- s = traverse(e)
202
- "<p class='byline'>#{s}</p>"
203
- end
204
-
205
- def handle_cell(e)
206
- doc = Nokogiri::XML::Document.new
207
- cell = doc.create_element('td')
208
- cell['rowspan'] = e['rows'] if e.key? 'rows'
209
- cell['colspan'] = e['cols'] if e.key? 'cols'
210
- cell.inner_html = traverse(e)
211
- to_html(cell)
212
- end
213
-
214
-
215
- def handle_corr(e)
216
- "<span class='corr'>%s</span>" % traverse(e)
217
- end
218
-
219
- def handle_div(e)
220
- if e.has_attribute? 'type'
221
- @open_divs << e
222
- r = traverse(e)
223
- @open_divs.pop
224
- return "<div>#{r}</div>"
225
- else
226
- return traverse(e)
227
- end
228
- end
229
-
230
- def handle_doc_number(e)
231
- "<p>%s</p>" % traverse(e)
232
- end
233
-
234
- def handle_figure(e)
235
- "<div class='figure'>%s</div>" % traverse(e)
236
- end
237
-
238
- def handle_foreign(e)
239
- return '' if e.key?('place') and e['place'].include?('foot')
240
- traverse(e)
241
- end
242
-
243
- def handle_g(e, mode)
244
- # 悉曇字、蘭札體 使用圖檔
245
- # 如果有對應的 unicode 且不在 Unicode Extension C, D, E 範圍裡,直接採用 unicode
246
- # 呈現組字式
247
- gid = e['ref'][1..-1]
248
- g = @gaijis[gid]
249
- abort "Line:#{__LINE__} 無缺字資料:#{gid}" if g.nil?
250
- zzs = g['zzs']
251
-
252
- if mode == 'txt'
253
- return g['roman'] if gid.start_with?('SD')
254
- if zzs.nil?
255
- abort "缺組字式:#{g}"
256
- else
257
- return zzs
258
- end
259
- end
260
-
261
- if gid.start_with?('SD')
262
- case gid
263
- when 'SD-E35A'
264
- return '('
265
- when 'SD-E35B'
266
- return ')'
267
- else
268
- fn = "#{gid}.gif"
269
- src = File.join(@config[:graphic_base], 'sd-gif', gid[3..4], fn)
270
- copy_file(src)
271
- return "<img src='#{fn}'/>"
272
- end
273
- end
274
-
275
- if gid.start_with?('RJ')
276
- fn = "#{gid}.gif"
277
- src = File.join(@config[:graphic_base], 'rj-gif', gid[3..4], fn)
278
- copy_file(src)
279
- return "<img src='#{fn}'/>"
280
- end
281
-
282
- if g.has_key?('unicode')
283
- # 如果不在 unicode ext-C, ext-D, ext-E 範圍內
284
- unless (0x2A700..0x2CEAF).include? g['unicode'].hex
285
- return g['unicode-char'] # 直接採用 unicode
286
- end
287
- end
288
-
289
- zzs
290
- end
291
-
292
- def handle_graphic(e)
293
- url = e['url']
294
- url.sub!(/^.*(figures\/.*)$/, '\1')
295
-
296
- src = File.join(@config[:graphic_base], url)
297
- copy_file(src)
298
-
299
- fn = File.basename(src)
300
- "<img src='#{fn}'/>"
301
- end
302
-
303
- def handle_head(e)
304
- if e['type'] == 'added'
305
- return ''
306
- elsif e.parent.name == 'list'
307
- return traverse(e)
308
- else
309
- i = @open_divs.size
310
- if i <= 6
311
- return "<p class='h#{i}'>%s</p>" % traverse(e)
312
- else
313
- return "<p class='h#{i}'>%s</p>" % traverse(e)
314
- end
315
- end
316
- end
317
-
318
- def handle_item(e)
319
- "<li>%s</li>\n" % traverse(e)
320
- end
321
-
322
- def handle_juan(e)
323
- "<p class='juan'>%s</p>" % traverse(e)
324
- end
325
-
326
- def handle_l(e)
327
- if @lg_type == 'abnormal'
328
- return traverse(e)
329
- end
330
-
331
- @in_l = true
332
-
333
- doc = Nokogiri::XML::Document.new
334
- cell = doc.create_element('div')
335
- cell['class'] = 'lg-cell'
336
- cell.inner_html = traverse(e) + ' '
337
-
338
- if e.key? 'rend'
339
- cell['style'] = e['rend']
340
- elsif @first_l
341
- parent = e.parent()
342
- if parent.has_attribute?('rend')
343
- indent = parent['rend'].scan(/text-indent:[^:]*/)
344
- unless indent.empty?
345
- cell['style'] = indent[0]
346
- end
347
- end
348
- end
349
- @first_l = false
350
- r = to_html(cell)
351
-
352
- unless @lg_row_open
353
- r = "\n<div class='lg-row'>" + r
354
- @lg_row_open = true
355
- end
356
- @in_l = false
357
- r
358
- end
359
-
360
- def handle_lb(e)
361
- return '' if e['type']=='old'
362
-
363
- # 卍續藏有 X 跟 R 兩種 lb, 只處理 X
364
- return '' if e['ed'] != @series
365
-
366
- r = ''
367
- if @lg_row_open && !@in_l
368
- # 每行偈頌放在一個 lg-row 裡面
369
- # T46n1937, p. 914a01, l 包雙行夾註跨行
370
- # T20n1092, 337c16, lb 在 l 中間,不結束 lg-row
371
- r += "</div><!-- end of lg-row -->"
372
- @lg_row_open = false
373
- end
374
- unless @t_buf1.empty? and @t_buf2.empty?
375
- r += print_tt
376
- end
377
- r
378
- end
379
-
380
- def handle_lem(e)
381
- r = nil
382
- w = e['wit']
383
- if e.key? 'wit'
384
- if (w.include? 'CBETA') and (not w.include? @orig)
385
- r = "<span class='corr'>%s</span>" % traverse(e)
386
- end
387
- end
388
- r = traverse(e) if r.nil?
389
- r
390
- end
391
-
392
- def handle_lg(e)
393
- r = ''
394
- @lg_type = e['type']
395
- if @lg_type == 'abnormal'
396
- r = "<p class='lg-abnormal'>" + traverse(e) + "</p>"
397
- else
398
- @first_l = true
399
- doc = Nokogiri::XML::Document.new
400
- node = doc.create_element('div')
401
- node['class'] = 'lg'
402
- if e.has_attribute?('rend')
403
- rend = e['rend'].gsub(/text-indent:[^:]*/, '')
404
- node['style'] = rend
405
- end
406
- @lg_row_open = false
407
- node.inner_html = traverse(e)
408
- if @lg_row_open
409
- node.inner_html += '</div><!-- end of lg -->'
410
- @lg_row_open = false
411
- end
412
- r = "\n" + to_html(node)
413
- end
414
- r
415
- end
416
-
417
- def handle_list(e)
418
- doc = Nokogiri::XML::Document.new
419
- node = doc.create_element('ul')
420
- if e.key? 'rendition'
421
- node['class'] = e['rendition']
422
- end
423
- node.inner_html = traverse(e)
424
- to_html(node) + "\n"
425
- end
426
-
427
- def handle_milestone(e)
428
- ''
429
- end
430
-
431
- def handle_mulu(e)
432
- return '' if e['type']=='卷'
433
- @mulu_count += 1
434
- level = e['level'].to_i
435
- while @current_nav.size > level
436
- @current_nav.pop
437
- end
438
-
439
- label = traverse(e, 'txt')
440
- li = @current_nav.last.add_child("<li><a href='#mulu#{@mulu_count}'>#{label}</a></li>").first
441
- ul = li.add_child('<ul></ul>').first
442
- @current_nav << ul
443
-
444
- # mulu 標記裡要有東西,prince 才會產生 pdf bookmark
445
- "<a id='mulu#{@mulu_count}'></a><mulu#{level} title='#{label}'>&nbsp;</mulu#{level}>"
446
- end
447
-
448
- def handle_node(e, mode)
449
- return '' if e.comment?
450
- return handle_text(e, mode) if e.text?
451
- return '' if PASS.include?(e.name)
452
- r = case e.name
453
- when 'anchor' then handle_anchor(e)
454
- when 'app' then handle_app(e)
455
- when 'byline' then handle_byline(e)
456
- when 'cell' then handle_cell(e)
457
- when 'corr' then handle_corr(e)
458
- when 'docNumber' then handle_doc_number(e)
459
- when 'div' then handle_div(e)
460
- when 'figure' then handle_figure(e)
461
- when 'foreign' then handle_foreign(e)
462
- when 'g' then handle_g(e, mode)
463
- when 'graphic' then handle_graphic(e)
464
- when 'head' then handle_head(e)
465
- when 'item' then handle_item(e)
466
- when 'juan' then handle_juan(e)
467
- when 'l' then handle_l(e)
468
- when 'lb' then handle_lb(e)
469
- when 'lem' then handle_lem(e)
470
- when 'lg' then handle_lg(e)
471
- when 'list' then handle_list(e)
472
- when 'mulu' then handle_mulu(e)
473
- when 'note' then handle_note(e)
474
- when 'milestone' then handle_milestone(e)
475
- when 'p' then handle_p(e)
476
- when 'rdg' then ''
477
- when 'reg' then ''
478
- when 'row' then handle_row(e)
479
- when 'sic' then ''
480
- when 'sg' then handle_sg(e)
481
- when 't' then handle_t(e)
482
- when 'tt' then handle_tt(e)
483
- when 'table' then handle_table(e)
484
- when 'unclear' then handle_unclear(e)
485
- else traverse(e)
486
- end
487
- r
488
- end
489
-
490
- def handle_note(e)
491
- if e.has_attribute?('type')
492
- t = e['type']
493
- if %w(equivalent orig orig_biao orig_ke mod rest).include? t
494
- return ''
495
- end
496
- return '' if t.start_with?('cf')
497
- end
498
-
499
- if e.has_attribute?('resp')
500
- return '' if e['resp'].start_with? 'CBETA'
501
- end
502
-
503
- r = traverse(e)
504
- if e.has_attribute?('place')
505
- if e['place']=='inline'
506
- r = "(#{r})"
507
- elsif e['place']=='interlinear'
508
- r = "(#{r})"
509
- end
510
- end
511
- r
512
- end
513
-
514
- def handle_p(e)
515
- doc = Nokogiri::XML::Document.new
516
- node = doc.create_element('p')
517
- if e.key? 'rend'
518
- node['style'] = e['rend']
519
- end
520
- node.inner_html = traverse(e)
521
- to_html(node) + "\n"
522
- end
523
-
524
- def handle_row(e)
525
- "<tr>" + traverse(e) + "</tr>\n"
526
- end
527
-
528
- def handle_sg(e)
529
- '(' + traverse(e) + ')'
530
- end
531
-
532
- def handle_sutra(xml_fn)
533
- puts "convert sutra #{xml_fn}"
534
-
535
- before_parse_xml(xml_fn)
536
-
537
- @text = parse_xml(xml_fn)
538
-
539
- # 目次
540
- if @config[:back_page_title]
541
- s = @config[:back_page_title]
542
- @nav_root.add_child("<li><a href='#back'>#{s}</a></li>")
543
- end
544
- @toc = to_html(@nav_root)
545
- @toc.gsub!('<ul/>', '')
546
-
547
- if @config.key? :graphic_base
548
-
549
- end
550
-
551
- if @config.key? :front_page
552
- s = File.read(@config[:front_page])
553
- @front = "<div id='front'>#{s}</div>"
554
- end
555
-
556
- if @config.key? :back_page
557
- s = File.read(@config[:back_page])
558
- @back = "<div id='back'>#{s}</div>"
559
- end
560
-
561
- fn = File.join(CBETA::DATA, 'pdf-template.htm')
562
- template = File.read(fn)
563
- output = template % {
564
- title: @title,
565
- author: @author,
566
- toc: @toc,
567
- front: @front,
568
- text: @text,
569
- back: @back
570
- }
571
-
572
- fn = File.join(@output_folder_sutra, 'main.htm')
573
- File.write(fn, output)
574
- end
575
-
576
- def handle_t(e)
577
- if e.has_attribute? 'place'
578
- return '' if e['place'].include? 'foot'
579
- end
580
- r = traverse(e)
581
-
582
- # <tt type="app"> 不是 悉漢雙行對照
583
- return r if @tt_type == 'app'
584
-
585
- # 處理雙行對照
586
- i = e.xpath('../t').index(e)
587
- case i
588
- when 0
589
- @t_buf1 << r
590
- when 1
591
- @t_buf2 << r
592
- else
593
- return r
594
- end
595
- ''
596
- end
597
-
598
- def handle_tt(e)
599
- @tt_type = e['type']
600
- traverse(e)
601
- end
602
-
603
- def handle_table(e)
604
- "<table>" + traverse(e) + "</table>\n"
605
- end
606
-
607
- def handle_text(e, mode)
608
- s = e.content().chomp
609
- return '' if s.empty?
610
- return '' if e.parent.name == 'app'
611
-
612
- # cbeta xml 文字之間會有多餘的換行
613
- r = s.gsub(/[\n\r]/, '')
614
-
615
- # 把 & 轉為 &amp;
616
- r = CGI.escapeHTML(r)
617
-
618
- r
619
- end
620
-
621
- def handle_unclear(e)
622
- '▆'
623
- end
624
-
625
- def handle_vol(vol)
626
- puts "convert volumn: #{vol}"
627
-
628
- @orig = @cbeta.get_canon_abbr(vol[0])
629
- abort "未處理底本" if @orig.nil?
630
- puts "#{__LINE__} orig: #{@orig}"
631
-
632
- @vol = vol
633
- @series = CBETA.get_canon_from_vol(vol)
634
- @out_folder = File.join(@out_root, @series, vol)
635
- FileUtils.remove_dir(@out_folder, true)
636
- FileUtils::mkdir_p @out_folder
637
-
638
- source = File.join(@xml_root, @series, vol)
639
- Dir.entries(source).sort.each { |f|
640
- next if f.start_with? '.'
641
- path = File.join(source, f)
642
- handle_sutra(path)
643
- }
644
- end
645
-
646
- def handle_vols(v1, v2)
647
- puts "convert volumns: #{v1}..#{v2}"
648
- @series = CBETA.get_canon_from_vol(v1)
649
- folder = File.join(@xml_root, @series)
650
- Dir.foreach(folder) { |vol|
651
- next if vol < v1
652
- next if vol > v2
653
- handle_vol(vol)
654
- }
655
- end
656
-
657
- def open_xml(fn)
658
- s = File.read(fn)
659
-
660
- if fn.include? 'T16n0657'
661
- # 這個地方 雙行夾註 跨兩行偈頌
662
- # 把 lb 移到 note 結束之前
663
- # 讓 lg-row 先結束,再結束雙行夾註
664
- s.sub!(/(<\/note>)(\n<lb n="0206b29" ed="T"\/>)/, '\2\1')
665
- end
666
-
667
- doc = Nokogiri::XML(s)
668
- doc.remove_namespaces!()
669
- doc
670
- end
671
-
672
- def parse_xml(xml_fn)
673
- @pass = [false]
674
-
675
- doc = open_xml(xml_fn)
676
-
677
- e = doc.xpath("//titleStmt/title")[0]
678
- @title = traverse(e, 'txt')
679
- @title = @title.split()[-1]
680
-
681
- @author = doc.at_xpath("//titleStmt/author").text
682
-
683
- if @cover.nil?
684
- @cover = "<p class='title'>#{@title}</p>\n"
685
- @cover += "<p class='author'>#{@author}</p>"
686
- end
687
-
688
- e = doc.at_xpath("//editionStmt/edition/date")
689
- abort "找不到版本日期" if e.nil?
690
- @edition_date = e.text.sub(/\$Date: (.*?) \$$/, '\1')
691
-
692
- e = doc.at_xpath("//projectDesc/p[@lang='zh']")
693
- abort "找不到貢獻者" if e.nil?
694
- @contributors = e.text
695
-
696
- root = doc.root()
697
- body = root.xpath("text/body")[0]
698
- @pass = [true]
699
-
700
- text = traverse(body)
701
- text
702
- end
703
-
704
- def print_tt
705
- r = "<table class='tt'>\n"
706
-
707
- r += "<tr>\n"
708
- @t_buf1.each do |s|
709
- r += "<td>#{s}</td>"
710
- end
711
- r += "</tr>\n"
712
-
713
- r += "<tr>\n"
714
- @t_buf2.each do |s|
715
- r += "<td>#{s}</td>"
716
- end
717
- r += "</tr>\n"
718
-
719
- @t_buf1 = []
720
- @t_buf2 = []
721
-
722
- r + "<table>\n"
723
- end
724
-
725
- def prepare_work_list(input_folder)
726
- Dir.foreach(input_folder) do |f|
727
- next if f.start_with? '.'
728
- p1 = File.join(input_folder, f)
729
- if File.file?(p1)
730
- work = f.sub(/^([A-Z]{1,2})\d{2,3}n(.*)\.xml$/, '\1\2')
731
- work = 'T0220' if work.start_with? 'T0220'
732
- unless @works.key? work
733
- @works[work] = []
734
- end
735
- @works[work] << p1
736
- else
737
- prepare_work_list(p1)
738
- end
739
- end
740
- end
741
-
742
- def traverse(e, mode='html')
743
- r = ''
744
- e.children.each { |c|
745
- s = handle_node(c, mode)
746
- r += s
747
- }
748
- r
749
- end
750
-
751
- end