# Copyright (c) 2008-2023 Minero Aoki, Kenshi Muto
# 2002-2007 Minero Aoki
#
# This program is free software.
# You can distribute or modify this program under the terms of
# the GNU LGPL, Lesser General Public License version 2.1.
#
require 'review/builder'
require 'review/htmlutils'
require 'review/textutils'
require 'nkf'
module ReVIEW
class IDGXMLBuilder < Builder
include TextUtils
include HTMLUtils
%i[ttbold hint maru keytop labelref ref strong em].each do |e|
Compiler.definline(e)
end
Compiler.defsingle(:dtp, 1)
Compiler.defblock(:insn, 0..1)
Compiler.defblock(:planning, 0..1)
Compiler.defblock(:best, 0..1)
Compiler.defblock(:security, 0..1)
Compiler.defblock(:point, 0..1)
Compiler.defblock(:shoot, 0..1)
Compiler.defblock(:reference, 0)
Compiler.defblock(:term, 0)
Compiler.defblock(:link, 0..1)
Compiler.defblock(:practice, 0)
Compiler.defblock(:expert, 0)
Compiler.defblock(:rawblock, 0)
def pre_paragraph
'
'
end
def post_paragraph
'
'
end
def extname
'.xml'
end
def builder_init_file
super
@warns = []
@errors = []
@section = 0
@subsection = 0
@subsubsection = 0
@subsubsubsection = 0
@sec_counter = SecCounter.new(5, @chapter)
@column = 0
@noindent = nil
@ol_num = nil
@first_line_num = nil
@rootelement = 'doc'
@tsize = nil
@texblockequation = 0
@texinlineequation = 0
print %Q(\n)
print %Q(<#{@rootelement} xmlns:aid="http://ns.adobe.com/AdobeInDesign/4.0/">)
@secttags = @book.config['structuredxml']
end
private :builder_init_file
def puts(arg)
if @book.config['nolf'].present?
print arg
else
super
end
end
def result
check_printendnotes
s = ''
if @secttags
s += '' if @subsubsubsection > 0
s += '' if @subsubsection > 0
s += '' if @subsection > 0
s += '' if @section > 0
s += ''
end
solve_nest(@output.string) + s + "#{@rootelement}>\n"
end
def solve_nest(s)
check_nest
s.gsub("\x01→dl←\x01", '').
gsub("\x01→/dl←\x01", "←END\x01").
gsub("\x01→ul←\x01", '').
gsub("\x01→/ul←\x01", "←END\x01").
gsub("\x01→ol←\x01", '').
gsub("\x01→/ol←\x01", "←END\x01").
gsub("←END\x01
", '').
gsub("←END\x01
", '').
gsub("←END\x01", '').
gsub("←END\x01", '')
end
def headline(level, label, caption)
output_close_sect_tags(level)
case level
when 1
print %Q() if @secttags
@section = 0
@subsection = 0
@subsubsection = 0
@subsubsubsection = 0
when 2
@section += 1
print %Q() if @secttags
@subsection = 0
@subsubsection = 0
@subsubsubsection = 0
when 3
@subsection += 1
print %Q() if @secttags
@subsubsection = 0
@subsubsubsection = 0
when 4
@subsubsection += 1
print %Q() if @secttags
@subsubsubsection = 0
when 5
@subsubsubsection += 1
print %Q() if @secttags
when 6
# ignore
else
raise "caption level too deep or unsupported: #{level}"
end
prefix, _anchor = headline_prefix(level)
label = label.nil? ? '' : %Q( id="#{label}")
toccaption = escape(compile_inline(caption.gsub(/@\{.+?\}/, '')).gsub(/<[^>]+>/, ''))
puts %Q(#{prefix}#{compile_inline(caption)})
end
def output_close_sect_tags(level)
if @secttags
if level <= 5 && @subsubsubsection > 0
print ''
end
if level <= 4 && @subsubsection > 0
print ''
end
if level <= 3 && @subsection > 0
print ''
end
if level <= 2 && @section > 0
print ''
end
end
end
def ul_begin
level = block_given? ? yield : ''
level = nil if level == 1
puts "
"
end
def ul_item_begin(lines)
print %Q(
#{join_lines_to_paragraph(lines).chomp})
end
def ul_item_end
puts '
'
end
def choice_single_begin
puts %Q()
end
def choice_multi_begin
puts %Q()
end
def choice_single_end
puts ''
end
def choice_multi_end
puts ''
end
def ul_end
level = block_given? ? yield : ''
level = nil if level == 1
puts "
"
end
def ol_begin
puts ''
@ol_num ||= 1 # rubocop:disable Naming/MemoizedInstanceVariableName
end
def ol_item(lines, num)
puts %Q(
#{join_lines_to_paragraph(lines).chomp}
)
@ol_num += 1
end
def ol_end
puts ''
@ol_num = nil
end
def olnum(num)
@ol_num = num.to_i
end
def dl_begin
puts '
'
end
def dt(line)
puts "
#{line}
"
end
def dd(lines)
puts "
#{join_lines_to_paragraph(lines).chomp}
"
end
def dl_end
puts '
'
end
def paragraph(lines)
if @noindent.nil?
if lines[0] =~ /\A(\t+)/
puts %Q(
)
end
end
def codelines_body(lines)
no = 1
lines.each do |line|
if @book.config['listinfo']
print %Q('
end
print detab(line)
print "\n"
print '' if @book.config['listinfo']
no += 1
end
end
def list(lines, id, caption, lang = nil)
puts ''
super(lines, id, caption, lang)
puts ''
end
def list_body(_id, lines, _lang)
print '
'
codelines_body(lines)
print '
'
end
def emlist(lines, caption = nil, _lang = nil)
quotedlist(lines, 'emlist', caption)
end
def emlistnum(lines, caption = nil, _lang = nil)
lines2 = []
first_line_num = line_num
lines.each_with_index do |line, i|
lines2 << detab(%Q() + (i + first_line_num).to_s.rjust(2) + ': ' + line)
end
quotedlist(lines2, 'emlistnum', caption)
end
def listnum(lines, id, caption, lang = nil)
puts ''
super(lines, id, caption, lang)
puts ''
end
def listnum_body(lines, _lang)
print '
'
no = 1
first_line_num = line_num
lines.each_with_index do |line, i|
if @book.config['listinfo']
print %Q('
end
print detab(%Q() + (i + first_line_num).to_s.rjust(2) + ': ' + line)
print "\n"
print '' if @book.config['listinfo']
no += 1
end
print '
'
end
def cmd(lines, caption = nil)
quotedlist(lines, 'cmd', caption)
end
def quotedlist(lines, css_class, caption)
print %Q()
if caption_top?('list') && caption.present?
puts "
#{compile_inline(caption)}
"
end
print '
'
no = 1
lines.each do |line|
if @book.config['listinfo']
print %Q('
end
print detab(line)
print "\n"
print '' if @book.config['listinfo']
no += 1
end
puts '
'
if !caption_top?('list') && caption.present?
puts "
#{compile_inline(caption)}
"
end
puts ''
end
private :quotedlist
def quote(lines)
blocked_lines = split_paragraph(lines)
puts "#{blocked_lines.join}"
end
def inline_table(id)
"#{super(id)}"
end
def inline_img(id)
"#{super(id)}"
end
def inline_eq(id)
"#{super(id)}"
end
def inline_imgref(id)
chapter, id = extract_chapter_id(id)
if chapter.image(id).caption.blank?
inline_img(id)
elsif get_chap(chapter).nil?
"#{I18n.t('image')}#{I18n.t('format_number_without_chapter', [chapter.image(id).number])}#{I18n.t('image_quote', chapter.image(id).caption)}"
else
"#{I18n.t('image')}#{I18n.t('format_number', [get_chap(chapter), chapter.image(id).number])}#{I18n.t('image_quote', chapter.image(id).caption)}"
end
end
def handle_metric(str)
k, v = str.split('=', 2)
%Q(#{k}="#{v.sub(/\A["']/, '').sub(/["']\Z/, '')}")
end
def result_metric(array)
" #{array.join(' ')}"
end
def image_image(id, caption, metric = nil)
metrics = parse_metric('idgxml', metric)
puts ''
image_header(id, caption) if caption_top?('image')
puts %Q()
image_header(id, caption) unless caption_top?('image')
puts ''
end
def image_dummy(id, caption, lines)
puts ''
image_header(id, caption) if caption_top?('image')
print %Q(
)
lines.each do |line|
print detab(line)
print "\n"
end
print '
'
image_header(id, caption) unless caption_top?('image')
puts ''
warn "image not bound: #{id}", location: location
end
def image_header(id, caption)
return true unless caption.present?
if get_chap.nil?
puts %Q(
)
end
puts caption_str if caption_top?('equation')
end
if @book.config['math_format'] == 'imgmath'
fontsize = @book.config['imgmath_options']['fontsize'].to_f
lineheight = @book.config['imgmath_options']['lineheight'].to_f
math_str = "\\begin{equation*}\n\\fontsize{#{fontsize}}{#{lineheight}}\\selectfont\n#{lines.join("\n")}\n\\end{equation*}\n"
key = Digest::SHA256.hexdigest(math_str)
img_path = @img_math.defer_math_image(math_str, key)
puts ''
puts %Q()
puts ''
else
puts %Q()
puts '
'
puts lines.join("\n")
puts '
'
puts ''
end
if id
puts caption_str unless caption_top?('equation')
puts ''
end
end
def table(lines, id = nil, caption = nil)
@tablewidth = nil
if @book.config['tableopt']
@tablewidth = @book.config['tableopt'].split(',')[0].to_f / @book.config['pt_to_mm_unit'].to_f # rubocop:disable Style/FloatDivision
end
@col = 0
sepidx, rows = parse_table_rows(lines)
puts '
'
begin
if caption_top?('table') && caption.present?
table_header(id, caption)
end
if @tablewidth.nil?
print ''
else
print %Q()
end
@table_id = id
table_rows(sepidx, rows)
puts ''
if !caption_top?('table') && caption.present?
table_header(id, caption)
end
rescue KeyError
app_error "no such table: #{id}"
end
puts '
'
@tsize = nil
end
def parse_table_rows(lines)
sepidx = nil
rows = []
lines.each_with_index do |line, idx|
if /\A[=-]{12}/.match?(line)
sepidx ||= idx
next
end
if @tablewidth
rows.push(line.gsub(/\t\.\t/, "\tDUMMYCELLSPLITTER\t").gsub(/\t\.\.\t/, "\t.\t").gsub(/\t\.\Z/, "\tDUMMYCELLSPLITTER").gsub(/\t\.\.\Z/, "\t.").gsub(/\A\./, ''))
else
rows.push(line.gsub(/\t\.\t/, "\t\t").gsub(/\t\.\.\t/, "\t.\t").gsub(/\t\.\Z/, "\t").gsub(/\t\.\.\Z/, "\t.").gsub(/\A\./, ''))
end
col2 = rows[rows.length - 1].split(table_row_separator_regexp).length
@col = col2 if col2 > @col
end
app_error 'no rows in the table' if rows.empty?
[sepidx, rows]
end
def table_rows(sepidx, rows)
cellwidth = []
if @tablewidth
if @tsize.nil?
@col.times { |n| cellwidth[n] = @tablewidth / @col }
else
cellwidth = @tsize.split(/\s*,\s*/)
totallength = 0
cellwidth.size.times do |n|
cellwidth[n] = cellwidth[n].to_f / @book.config['pt_to_mm_unit'].to_f # rubocop:disable Style/FloatDivision
totallength += cellwidth[n]
warn "total length exceeds limit for table: #{@table_id}", location: location if totallength > @tablewidth
end
if cellwidth.size < @col
cw = (@tablewidth - totallength) / (@col - cellwidth.size)
warn "auto cell sizing exceeds limit for table: #{@table_id}", location: location if cw <= 0
(cellwidth.size..(@col - 1)).each { |i| cellwidth[i] = cw }
end
end
end
if sepidx
sepidx.times do |y|
if @tablewidth.nil?
puts %Q(
#{rows.shift}
)
else
i = 0
rows.shift.split(table_row_separator_regexp).each_with_index do |cell, x|
print %Q(
#{cell.sub('DUMMYCELLSPLITTER', '')}
)
i += 1
end
end
end
end
trputs(@tablewidth, rows, cellwidth, sepidx)
end
def trputs(tablewidth, rows, cellwidth, sepidx)
sepidx = 0 if sepidx.nil?
if tablewidth
rows.each_with_index do |row, y|
i = 0
row.split(table_row_separator_regexp).each_with_index do |cell, x|
print %Q(
#{cell.sub('DUMMYCELLSPLITTER', '')}
)
i += 1
end
end
else
lastline = rows.pop
rows.each { |row| puts "
#{row}
" }
puts %Q(
#{lastline}
) if lastline
end
end
def table_header(id, caption)
if id.nil?
puts %Q(
)
end
end
def table_begin(ncols)
end
def tr(rows)
puts %Q(
#{rows.join("\t")}
)
end
def th(str)
%Q(#{str})
end
def td(str)
str
end
def table_end
end
def emtable(lines, caption = nil)
table(lines, nil, caption)
end
def imgtable(lines, id, caption = nil, metric = nil)
if @chapter.image_bound?(id)
metrics = parse_metric('idgxml', metric)
puts '
'
if caption_top?('table') && caption.present?
table_header(id, caption)
end
puts %Q()
if !caption_top?('table') && caption.present?
table_header(id, caption)
end
puts '
'
else
warn "image not bound: #{id}", location: location if @strict
image_dummy(id, caption, lines)
end
end
def comment(lines, comment = nil)
return unless @book.config['draft']
lines ||= []
lines.unshift(escape(comment)) unless comment.blank?
str = lines.join("\n")
print "#{str}"
end
def inline_comment(str)
if @book.config['draft']
%Q(#{escape(str)})
else
''
end
end
def footnote(id, str)
# see inline_fn
end
def inline_fn(id)
%Q(#{compile_inline(@chapter.footnote(id).content.strip)})
rescue KeyError
app_error "unknown footnote: #{id}"
end
def inline_endnote(id)
%Q((#{@chapter.endnote(id).number}))
rescue KeyError
app_error "unknown endnote: #{id}"
end
def endnote_begin
puts ''
end
def endnote_end
puts ''
end
def endnote_item(id)
puts %Q((#{@chapter.endnote(id).number})\t#{compile_inline(@chapter.endnote(id).content)})
end
def compile_ruby(base, ruby)
%Q(#{escape(base)}#{escape(ruby)})
end
def compile_kw(word, alt)
'' +
if alt
escape("#{word}(#{alt.strip})")
else
escape(word)
end +
'' +
%Q() +
if alt
alt.split(/\s*,\s*/).collect! { |e| %Q() }.join
else
''
end
end
def compile_href(url, label)
%Q(#{label.nil? ? escape(url) : escape(label)})
end
def inline_sup(str)
%Q(#{escape(str)})
end
def inline_sub(str)
%Q(#{escape(str)})
end
def inline_raw(str)
super(str).gsub('\\n', "\n")
end
def inline_hint(str)
if @book.config['nolf']
%Q(#{escape(str)})
else
%Q(\n#{escape(str)})
end
end
def inline_maru(str)
if /\A\d+\Z/.match?(str)
sprintf('%x;', 9311 + str.to_i)
elsif /\A[A-Z]\Z/.match?(str)
begin
sprintf('%x;', 9398 + str.codepoints.to_a[0] - 65)
rescue NoMethodError
sprintf('%x;', 9398 + str[0] - 65)
end
elsif /\A[a-z]\Z/.match?(str)
begin
sprintf('%x;', 9392 + str.codepoints.to_a[0] - 65)
rescue NoMethodError
sprintf('%x;', 9392 + str[0] - 65)
end
else
app_error "can't parse maru: #{str}"
end
end
def inline_idx(str)
%Q(#{escape(str)})
end
def inline_hidx(str)
%Q()
end
def inline_ami(str)
%Q(#{escape(str)})
end
def inline_i(str)
%Q(#{escape(str)})
end
def inline_b(str)
%Q(#{escape(str)})
end
def inline_em(str)
%Q(#{escape(str)})
end
def inline_strong(str)
%Q(#{escape(str)})
end
def inline_tt(str)
%Q(#{escape(str)})
end
def inline_ttb(str)
%Q(#{escape(str)})
end
alias_method :inline_ttbold, :inline_ttb
def inline_tti(str)
%Q(#{escape(str)})
end
def inline_u(str)
%Q(#{escape(str)})
end
def inline_ins(str)
%Q(#{escape(str)})
end
def inline_del(str)
%Q(#{escape(str)})
end
def inline_icon(id)
begin
%Q()
rescue StandardError
warn "image not bound: #{id}", location: location
''
end
end
def inline_bou(str)
%Q(#{escape(str)})
end
def inline_keytop(str)
%Q(#{escape(str)})
end
def inline_labelref(idref)
%Q(「#{I18n.t('label_marker')}#{escape(idref)}」) # FIXME: 節名とタイトルも込みで要出力
end
alias_method :inline_ref, :inline_labelref
def inline_pageref(idref)
%Q(●●) # ページ番号を参照
end
def inline_balloon(str)
%Q(#{escape(str).gsub(/@maru\[(\d+)\]/) { inline_maru($1) }})
end
def inline_uchar(str)
%Q(#{str};)
end
def inline_m(str)
@texinlineequation += 1
if @book.config['math_format'] == 'imgmath'
math_str = '$' + str + '$'
key = Digest::SHA256.hexdigest(str)
img_path = @img_math.defer_math_image(math_str, key)
%Q()
else
%Q(
#{escape(str)}
)
end
end
def noindent
@noindent = true
end
def blankline
puts ''
end
def pagebreak
puts ''
end
def nonum_begin(level, _label, caption)
puts %Q(#{compile_inline(caption)})
end
def nonum_end(level)
end
def notoc_begin(level, _label, caption)
puts %Q(#{compile_inline(caption)})
end
def notoc_end(level)
end
def nodisp_begin(level, label, caption)
end
def nodisp_end(level)
end
def circle_begin(_level, _label, caption)
puts %Q(•#{compile_inline(caption)})
end
def circle_end(level)
end
def common_column_begin(type, caption)
@column += 1
a_id = %Q(id="column-#{@column}")
print "<#{type}column #{a_id}>"
puts %Q(#{compile_inline(caption)})
end
def common_column_end(type)
puts "#{type}column>"
end
def column_begin(_level, _label, caption)
common_column_begin('', caption)
end
def column_end(_level)
common_column_end('')
end
def xcolumn_begin(_level, _label, caption)
common_column_begin('x', caption)
end
def xcolumn_end(_level)
common_column_end('x')
end
def world_begin(_level, _label, caption)
common_column_begin('world', caption)
end
def world_end(_level)
common_column_end('world')
end
def hood_begin(_level, _label, caption)
common_column_begin('hood', caption)
end
def hood_end(_level)
common_column_end('hood')
end
def edition_begin(_level, _label, caption)
common_column_begin('edition', caption)
end
def edition_end(_level)
common_column_end('edition')
end
def insideout_begin(_level, _label, caption)
common_column_begin('insideout', caption)
end
def insideout_end(_level)
common_column_end('insideout')
end
def ref_begin(_level, label, _caption)
if label
puts ""
else
puts ''
end
end
def ref_end(_level)
puts ''
end
def sup_begin(_level, label, _caption)
if label
puts ""
else
puts ''
end
end
def sup_end(_level)
puts ''
end
def flushright(lines)
puts split_paragraph(lines).join.gsub('
', %Q(
))
end
def centering(lines)
puts split_paragraph(lines).join.gsub('
#{compile_inline(caption)}" if caption.present?
blocked_lines = split_paragraph(lines)
puts "#{blocked_lines.join}#{type}>"
end
def note(lines, caption = nil)
check_nested_minicolumn
captionblock('note', lines, caption)
end
def memo(lines, caption = nil)
check_nested_minicolumn
captionblock('memo', lines, caption)
end
def tip(lines, caption = nil)
check_nested_minicolumn
captionblock('tip', lines, caption)
end
def info(lines, caption = nil)
check_nested_minicolumn
captionblock('info', lines, caption)
end
def planning(lines, caption = nil)
captionblock('planning', lines, caption)
end
def best(lines, caption = nil)
captionblock('best', lines, caption)
end
def important(lines, caption = nil)
check_nested_minicolumn
captionblock('important', lines, caption)
end
def security(lines, caption = nil)
captionblock('security', lines, caption)
end
def caution(lines, caption = nil)
check_nested_minicolumn
captionblock('caution', lines, caption)
end
def warning(lines, caption = nil)
check_nested_minicolumn
captionblock('warning', lines, caption)
end
def term(lines)
captionblock('term', lines, nil)
end
def link(lines, caption = nil)
captionblock('link', lines, caption)
end
def notice(lines, caption = nil)
check_nested_minicolumn
if caption
captionblock('notice-t', lines, caption, 'notice-title')
else
captionblock('notice', lines, nil)
end
end
def point(lines, caption = nil)
if caption
captionblock('point-t', lines, caption, 'point-title')
else
captionblock('point', lines, nil)
end
end
def shoot(lines, caption = nil)
if caption
captionblock('shoot-t', lines, caption, 'shoot-title')
else
captionblock('shoot', lines, nil)
end
end
def reference(lines)
captionblock('reference', lines, nil)
end
def practice(lines)
captionblock('practice', lines, nil)
end
def expert(lines)
captionblock('expert', lines, nil)
end
CAPTION_TITLES.each do |name|
class_eval %Q(
def #{name}_begin(caption = nil)
check_nested_minicolumn
if '#{name}' == 'notice' && caption.present?
@doc_status[:minicolumn] = '#{name}-t'
print "<#{name}-t>"
else
@doc_status[:minicolumn] = '#{name}'
print "<#{name}>"
end
if caption.present?
puts %Q(\#{compile_inline(caption)})
end
end
def #{name}_end
if '#{name}' == 'notice' && @doc_status[:minicolumn] == 'notice-t'
print "#{name}-t>"
else
print "#{name}>"
end
@doc_status[:minicolumn] = nil
end
), __FILE__, __LINE__ - 23
end
def syntaxblock(type, lines, caption)
captionstr = nil
if caption.present?
titleopentag = %Q(caption aid:pstyle="#{type}-title")
titleclosetag = 'caption'
if type == 'insn'
titleopentag = %Q(floattitle type="insn")
titleclosetag = 'floattitle'
end
captionstr = %Q(<#{titleopentag}>#{compile_inline(caption)}#{titleclosetag}>)
end
print "<#{type}>"
if caption_top?('list')
puts captionstr
else
puts ''
end
no = 1
lines.each do |line|
if @book.config['listinfo']
print %Q('
end
print detab(line)
print "\n"
print '' if @book.config['listinfo']
no += 1
end
unless caption_top?('list')
print captionstr
end
puts "#{type}>"
end
def insn(lines, caption = nil)
syntaxblock('insn', lines, caption)
end
def box(lines, caption = nil)
syntaxblock('box', lines, caption)
end
def indepimage(_lines, id, caption = nil, metric = nil)
metrics = parse_metric('idgxml', metric)
puts ''
if caption_top?('image') && caption.present?
puts %Q(
#{compile_inline(caption)}
)
end
begin
puts %Q()
rescue StandardError
warn %Q(image not bound: #{id}), location: location
end
if !caption_top?('image') && caption.present?
puts %Q(
#{compile_inline(caption)}
)
end
puts ''
end
alias_method :numberlessimage, :indepimage
def label(id)
# FIXME
print ""
end
def dtp(str)
print %Q()
end
def hr
print ''
end
def bpo(lines)
puts %Q(#{lines.join("\n")})
end
def inline_dtp(str)
""
end
def inline_code(str)
%Q(#{escape(str)})
end
def inline_br(_str)
"\n"
end
def rawblock(lines)
no = 1
lines.each do |l|
print l.gsub('<', '<').gsub('>', '>').gsub('"', '"').gsub('&', '&')
print "\n" unless lines.length == no
no += 1
end
end
def text(str)
str
end
def inline_chapref(id)
if @book.config.check_version('2', exception: false)
# backward compatibility
chs = ['', '「', '」']
if @book.config['chapref']
chs2 = @book.config['chapref'].split(',')
if chs2.size == 3
chs = chs2
else
app_error '--chapsplitter must have exactly 3 parameters with comma.'
end
end
s = "#{chs[0]}#{@book.chapter_index.number(id)}#{chs[1]}#{@book.chapter_index.title(id)}#{chs[2]}"
if @book.config['chapterlink']
%Q(#{s})
else
s
end
else
title = super
if @book.config['chapterlink']
%Q(#{title})
else
title
end
end
rescue KeyError
app_error "unknown chapter: #{id}"
end
def inline_chap(id)
if @book.config['chapterlink']
%Q(#{@book.chapter_index.number(id)})
else
@book.chapter_index.number(id)
end
rescue KeyError
app_error "unknown chapter: #{id}"
end
def inline_title(id)
title = super
if @book.config['chapterlink']
%Q(#{title})
else
title
end
rescue KeyError
app_error "unknown chapter: #{id}"
end
def source(lines, caption = nil, lang = nil)
puts ''
end
def source_header(caption)
puts %Q(
#{compile_inline(caption)}
) if caption.present?
end
def source_body(lines, _lang)
puts '