1 # (c) 2006-2013 Benjamin Crowell, GPL licensed
3 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
6 # Always edit the version of this file in /home/bcrowell/Documents/programming/eruby_util_for_books/eruby_util.rb --
7 # it will automatically get copied over into the various projects the next time I do a "make" or a
11 # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
13 # This script is used in everything except for Brief Calculus, which has a different layout.
15 # See INTERNALS for documentation on all the files: geom.pos, marg.pos, chNN.pos,
16 # figfeedbackNN, all.pos.
20 $label_counter = 0 # for generating labels when the user doesn't supply one
23 $hw_number_in_block = 0
25 $hw_block = 0 # for style used in Fundamentals of Calculus
28 $hw_names_referred_to = []
30 $tex_points_to_mm = (25.4)/(65536.*72.27)
33 $geom_file = "geom.pos"
36 $geom = [ 11.40 , 63.40 , 154.40 , 206.40 , 28.00 , 258.00]
37 # See geom() for the definitions of these numbers.
38 # These are just meant to be sane defaults to use if the geom.pos file hasn't been created yet.
39 # If they turn out to be wrong, or not even sane, that doesn't matter, because we'll be getting
40 # the right values on the next iteration. Actually it makes very little difference, because on the
41 # first iteration, we don't even know whether a particular figure is on a left page or a right page,
42 # so we don't even try to position it very well.
45 $marg_file = "marg.pos"
47 # ... an array of hashes
48 $read_feedback = false
49 $feedback_exists = nil
50 #... can't check for existence until the first marg() call, because we don't know $ch yet
51 $page_invoked_from = []
53 # a hash for keeping track of how many times a figure has been reused within the same chapter
54 $web_command_marker = 'ZZZWEB:'
56 $count_section_commands = 0
58 $section_label_stack = [] # see begin_sec() and end_sec(); unlabeled sections have ''
59 $section_title_stack = []
60 $section_most_recently_begun = nil # title of the section that was the most recent one successfully processed
61 $conditional_stack = []
63 def fatal_error(message)
64 $stderr.print "eruby_util.rb: #{message}\n"
65 $stderr.print stack_dump()
70 result = "section title stack = "+$section_title_stack.join(',')+"\n"
71 if !$section_most_recently_begun.nil? then
72 result = result + "most recent begin_sec successfully processed was for #{$section_most_recently_begun}\n"
77 def save_complaint(message)
78 File.open('eruby_complaints','a') { |f|
83 # returns contents or nil on error; for more detailed error reporting, see slurp_file_with_detailed_error_reporting()
85 x = slurp_file_with_detailed_error_reporting(file)
89 # returns [contents,nil] normally [nil,error message] otherwise
90 def slurp_file_with_detailed_error_reporting(file)
92 File.open(file,'r') { |f|
93 t = f.gets(nil) # nil means read whole file
94 if t.nil? then t='' end # gets returns nil at EOF, which means it returns nil if file is empty
98 return [nil,"Error opening file #{file} for input: #{$!}."]
103 #--------------------------------------------------------------------------
104 config_file = 'book.config'
105 if ! File.exist?(config_file) then fatal_error("error, file #{config_file} does not exist") end
107 # In the following, nil means that there is no default and it's an error if it's not given explicitly.
108 'titlecase_above'=>nil, # e.g., 1 means titlecase for chapters but not for sections or subsections
109 'hw_block_style'=>0 # 1 means hw numbered like a7, as in Fundamentals of Calculus
111 File.open(config_file,'r') { |f|
112 c = f.gets(nil) # nil means read whole file
113 c.scan(/(\w+),(.*)/) { |var,value|
114 if ! $config.has_key?(var) then fatal_error("Error in config file #{config_file}, illegal variable '#{var}'") end
115 if {'titlecase_above'=>nil,'hw_block_style'=>nil}.has_key?(var) then
121 $config.keys.each { |k|
122 if $config[k].nil? then fatal_error("error, variable #{k} not given in #{config_file}") end
125 #--------------------------------------------------------------------------
126 # The following code is a workaround for a bug in latex. The symptom is that I get
127 # "Missing \endcsname inserted" in a few isolated cases where I use a pageref inside
128 # the caption of a figure. See meki latex notes for more details. In these cases, I
129 # can get the refs using eruby instead of latex. See pageref_workaround() and ref_workaround() below.
130 # I also use this in LM, problems 3-2, 3-3, and 3-4, where they need to refer to the page where the blank form is given for sketching graphs;
131 # that doesn't work if I try to use the label associated with the floating figure, which points to the page from which it was invoked.
133 # Code similar to this is duplicated in translate_to_html.rb:
134 refs_file = 'save.ref'
137 if File.exist?(refs_file) then # It's not an error if the file doesn't exist yet; references are just not defined yet, and that's normal for the first time on a fresh file.
138 # lines look like this:
139 # fig:entropygraphb,h,255
140 t = slurp_file(refs_file)
141 t.scan(/(.*),(.*),(.*)/) { |label,number,page|
142 if $ref[label]!=nil then
143 if $last_chapter==true && $ref[label][0]!=number && $ref[label][1]!=page.to_i && label=~/\Afig:/ then
144 save_complaint("******* warning: figure #{label} defined both as figure #{$ref[label][0]} on p. #{$ref[label][1]} and as figure #{number} on p. #{page.to_i}, eruby_util.rb reading #{refs_file}")
147 $ref[label] = [number,page.to_i]
148 if n_defs[label]==nil then n_defs[label]=0 end
149 n_defs[label] = n_defs[label]+1
154 n_defs.keys.each {|fig|
156 avg = avg + n_defs[fig]
159 n_defs.keys.each {|fig|
160 #if n_defs[fig] > avg then $stderr.print "****** warning: figure #{fig} defined #{n_defs[fig]} times in save.ref, which is more than the average of #{avg}\n" end
163 def ref_workaround(label)
164 if $ref[label]==nil then return 'nn' end # The first time through, won't have a save.ref. Put in a placeholder that's about the right width.
165 return $ref[label][0]
168 def pageref_workaround(label)
169 if $ref[label]==nil then return 'nnn' end # The first time through, won't have a save.ref. Put in a placeholder that's about the right width.
170 return $ref[label][1].to_s
173 #--------------------------------------------------------------------------
175 # set by run_eruby.pl
176 # tells whether the book is calculus-based
177 # if set, ignore markers on hw and section in L&M for optional calc-based material
179 return ENV['CALC']=='1'
182 # set by run_eruby.pl
183 # for use when generating screen-resolution figures
184 # e.g., ../9share/optics
186 return [ENV['SHARED_FIGS'],ENV['SHARED_FIGS2']]
190 return ENV['BOOK_OUTPUT_FORMAT']!='web'
194 return ENV['BOOK_OUTPUT_FORMAT']=='web'
201 # argument can be 0, 1, true, or false; don't do, e.g., !__sn, because in ruby !0 is false
202 def begin_if(condition)
203 if condition.class() == Fixnum then
204 if condition==1 then condition=true else condition=false end
206 if condition.class()!=TrueClass && condition.class()!=FalseClass then
207 die('(begin_if)',"begin_if called with argument of class #{condition.class()}, should be Fixnum, true, or false")
209 $conditional_stack.push(condition)
211 print "\n\\begin{comment}\n" # requires comment package; newlines before and after are required by that package
216 condition = $conditional_stack.pop
218 print "\n\\end{comment}\n" # requires comment package; newlines before and after are required by that package
223 return "ch#{$ch}.pos"
226 def previous_pos_file
228 if p<0 then return nil end
229 if p<10 then p = '0'+p.to_s end
233 # returns data in units of mm, in the coordinate system used by pdfsavepos (positive y up)
235 if ! $checked_geom then
236 $geom_exists = File.exist?($geom_file)
238 File.open($geom_file,'r') do |f|
240 if !(line=~/pt/) then # make sure it's already been parsed into millimeters
247 index = {'evenfigminx'=>0,'evenfigmaxx'=>1,'oddfigminx'=>2,'oddfigmaxx'=>3,'figminy'=>4,'figmaxy'=>5}[what]
248 result = $geom[index].to_f
249 if what=='figmaxy' then result=result-2.5 end
254 if !$in_marg then die('(end_marg)',"end_marg, not in a marg in the first place, chapter #{$ch}") end
255 if is_print then print "\\end{textblock*}\\end{margin}%\n\\vspace{1.5mm}" end
256 if is_web then print "#{$web_command_marker}end_marg\n" end
261 if $in_marg then die('(marg)','marg, but already in a marg') end
264 if is_print then marg_print(delta_y) end
265 if is_web then print "#{$web_command_marker}marg\n" end
268 # sets $page_invoked_from[] as a side-effect
269 def marg_print(delta_y)
270 print "\\begin{margin}{#{$n_marg}}{#{delta_y}}{#{$ch}}%\n";
271 # (x,y) are in coordinate system used by pdfsavepos, with positive y up
272 miny = geom('figminy')
273 maxy = geom('figmaxy')
276 fig_file = "figfeedback#{$ch}"
277 if $feedback_exists==nil then $feedback_exists=File.exist?(fig_file) end
278 if $feedback_exists and !$read_feedback then
279 $read_feedback = true
280 File.open(fig_file,'r') do |f|
282 # line looks like this: 1,page=15,refx=6041561,refy=46929091,deltay=12
283 if line =~ /(\d+),page=(\d+),refx=(\-?\d+),refy=(\-?\d+),deltay=(\-?\d+)/ then
284 n,page,refx,refy,deltay=$1.to_i,$2.to_i,$3.to_i,$4.to_i,$5.to_i
285 $feedback[n] = {'n'=>n,'page'=>page,'refx'=>refx,'refy'=>refy,'deltay'=>deltay}
286 $page_invoked_from[n] = page
288 die(name,"syntax error in file #{fig_file}, line=#{line}")
292 File.delete(fig_file) # otherwise it grows by being appended to every time we run tex
294 if $feedback_exists then
295 feed = $feedback[$n_marg]
298 deltay = feed['deltay']
299 y = refy*$tex_points_to_mm+deltay
302 $stderr.print "miny=#{miny}\n" if debug
306 x=geom('evenfigminx') # left page
308 x=geom('oddfigminx') # right page
310 # The following are all in units of millimeters.
311 tol_out = 50 # if a figure is outside its allowed region by less than this, we fix it silently; if it's more than that, we give a warning
312 tol_in = 5 # if a figure is this close to the top or bottom, we silently snap it exactly to the top or bottom
313 max_fudge = 3 # amount by which a tall stack of figures can stick up over the top, if it's just plain too big to fit
314 min_ht = 15 # even if we don't know ht, all figures are assumed to be at least this high
315 if y>maxy+tol_out then warn_marg(1,$n_marg,page,"figure too high by #{mm(y-maxy)} mm, which is greater than #{mm(tol_out)} mm, ht=#{mm(ht)}") end
316 if y>maxy-tol_in then y=maxy end
318 $stderr.print "ht=#{ht}\n" if debug
319 if y-ht<miny-tol_out then warn_marg(1,$n_marg,page,"figure too low by #{mm(miny-(y-ht))} mm, which is greater than #{tol_out} mm, ht=#{mm(ht)}") end
321 # The stack of figures is simply too tall to fit. The user will get warned about this later, and may be doing it
322 # on purpose, as a last resort. Typically in this situation, what looks least bad is to align it at the top, or a tiny bit above.
324 if fudge>max_fudge then fudge=max_fudge end
327 if y-ht<miny+tol_in then y=miny+ht end
330 # A final sanity check, which has to work whether or not we know ht.
331 if y>maxy+max_fudge then y=maxy+tol_insane end
332 if y<miny+min_ht then y=miny+min_ht end
333 end # if fig_file exists
334 # In the following, I'm converting from pdfsavepos's coordinate system to textpos's; assumes calc package is available.
335 print "\\begin{textblock*}{\\marginfigwidth}(#{x}mm,\\paperheight-#{y}mm)%\n"
338 # options is normally {}
339 def marginbox(delta_y,name,caption,options,text)
340 options['text'] = text
341 options['textbox'] = true
343 fig(name,caption,options)
348 if x==nil then return '' end
349 return sprintf((x+0.5).to_i.to_s,"%d")
353 # 1 = figure too low or high by more than 50 mm
354 # I currently have no other types of warnings with higher severities.
355 # I currently only report severity>1, so the calls to warn_marg() with severity=1 are noops.
356 # The warnings with severity=1 were too copious, had too many false positives, and were
357 # obscuring other, more important errors. They seldom if ever succeeded in locating anything
358 # that was actually a problem.
359 # Checks for colliding figures, which are a serious error, happen in a separate
360 # script, check_for_colliding_figures.rb.
361 def warn_marg(severity,nmarg,page,message)
362 # First, figure out what figures are associated with the current margin block.
364 if File.exist?($marg_file) then
365 File.open($marg_file,'r') do |f|
367 if line=~/fig:(.*),nmarg=(\d+),ch=(\d+)/ then
368 fig,gr,ch = $1,$2.to_i,$3
369 mine[fig]=1 if (gr==nmarg.to_i && ch==$ch)
371 $stderr.print "error in #{$marg_file}, no match?? #{line}\n"
377 $stderr.print "warning, severity #{severity} nmarg=#{nmarg}, ch. #{$ch}, p. #{page}, #{mine.keys.join(',')}: #{message}\n"
382 if ! $checked_pos then
383 $pos_exists = File.exist?(pos_file())
389 # returns height in mm, or nil if the all.pos file doesn't exist yet, or figure not listed in it
391 #debug = ($ch.to_i==0 and $n_marg==6)
394 $stderr.print "debug is on, pos_file_exists=#{pos_file_exists()}, pos_file=#{pos_file()}, cwd=#{Dir.getwd()}\n"
395 $stderr.print "listing of *.pos = "+`ls *.pos`
397 if !(File.exist?($marg_file)) then return nil end
398 if !pos_file_exists() then return nil end
399 # First, figure out what figures are associated with the current margin block.
401 File.open($marg_file,'r') do |f|
402 # The file grows by appending with each iteration. If the user isn't modifying the tex file (drastically) between
403 # runs, then it should all just be exact repetition. If not, then we just use the freshest available data. At any given
404 # time, the latest chunk of the file will be incomplete, and the freshest data for some margin blocks could be either
405 # in the final chunk or in the penultimate one. There's some risk that something goofy could happen if the user
406 # does rearrange blocks between iterations. The file also mixes data from different chapters.
407 # ************ Bug: if the same figure is used in two different chapters, I think this will mess up **************************
408 # ************ It's inefficient to call this many times. ********************
410 if line=~/(.*),nmarg=(\d+),ch=(\d+)/ then
411 fig,gr,ch = $1,$2.to_i,$3
412 mine[fig] = 1 if (gr==$n_marg.to_i and ch==$ch)
413 $stderr.print "#{fig} is mine!\n" if debug and mine[fig]
417 $stderr.print "keys=" + (mine.keys.join(',')) + "\n" if debug
418 # Read the chNN.pos file, which typically looks like this:
419 # fig,label=fig:mass-on-spring,page=15,x=28790655,y=45437345,at=begin
420 # fig,label=fig:mass-on-spring,page=15,x=38486990,y=27308866,at=endgraphic
421 # fig,label=fig:mass-on-spring,page=15,x=38195719,y=22590274,at=endcaption
422 huge = 999/$tex_points_to_mm # 999 mm, expressed in units of tex points
427 found,lo_y,hi_y = get_low_and_hi!(found,lo_y,hi_y,pos_file(),mine)
429 # Very rarely (ch. 4 of genrel), I have a figure on the first page of a chapter, which gets written to the chNN.pos for the previous chapter.
430 # I think this happens because the write18 that renames all.pos isn't executed until after the first page of the new chapter is output.
431 # I don't know why this never happens in SN or LM; possibly because they have chapter opener photos that are big enough to cause buffers to get flushed?
432 # Checking previous_pos_file() seems to take care of this on the very rare occasions when it happens.
433 if !found and File.exist?(previous_pos_file()) then
434 found,lo_y,hi_y = get_low_and_hi!(found,lo_y,hi_y,previous_pos_file(),mine)
437 #warn_marg(1,$n_marg,0,"figure not found in height_of_marg, $n_marg=#{$n_marg} $ch=#{$ch}; see comment in eruby_util for more about this")
438 # This happens and is normal for wide figures, which are not in the margin. They appear in chNN.pos, but not in marg.pos.
441 if !found then return nil end
442 height = (hi_y - lo_y)*$tex_points_to_mm
443 #if height<1 then die('(height_of_marg)',"height #{height} is too small, lo=#{lo_y}, hi=#{hi_y}") end
444 if height<1 then return nil end #???????????????????????????????????????
445 $stderr.print "height=#{height}\n" if debug
449 def get_low_and_hi!(found,lo_y,hi_y,filename,mine)
450 File.open(filename,'r') do |f|
452 if line=~/^fig,label=(.*),page=(.*),x=(.*),y=(.*),at=(.*)/ then
453 fig,page,y=$1,$2.to_i,$4.to_i
454 if mine.has_key?(fig) then
455 if y<lo_y then lo_y = y end
456 if y>hi_y then hi_y = y end
465 def figure_exists_in_my_own_dir?(name)
466 return figure_exists_in_this_dir?(name,dir()+"/figs")
469 def figure_exists_in_this_dir?(name,d)
470 return (File.exist?("#{d}/#{name}.pdf") or File.exist?("#{d}/#{name}.jpg") or File.exist?("#{d}/#{name}.png"))
473 # returns a directory (possibly with LaTeX macros in it) or nil if we can't find the figure
474 def find_directory_where_figure_is(name)
475 if figure_exists_in_my_own_dir?(name) then return dir = "\\figprefix\\chapdir/figs" end
476 # bug: doesn't support \figprefix
478 if figure_exists_in_this_dir?(name,s[0]) then return s[0] end
479 if figure_exists_in_this_dir?(name,s[1]) then return s[1] end
483 def figure_in_toc(name,options={})
489 default_options.each {
491 if options[option]==nil then
492 options[option]=default
496 if !(File.exist?(d)) then d='front/figs' end
497 if !(File.exist?("#{d}/toc-#{name}.pdf") or File.exist?("#{d}/toc-#{name}.jpg") or File.exist?("#{d}/toc-#{name}.png")) then
500 if options['noresize'] then
501 print "\\addtocontents{toc}{\\protect\\figureintocnoresize{#{d}/toc-#{name}}}"
503 if options['scootx']==0 then
504 if options['scooty']==0 then
505 print "\\addtocontents{toc}{\\protect\\figureintoc{#{d}/toc-#{name}}}"
507 print "\\addtocontents{toc}{\\protect\\figureintocscooty{#{d}/toc-#{name}}{#{options['scooty']}mm}}"
510 print "\\addtocontents{toc}{\\protect\\figureintocscootx{#{d}/toc-#{name}}{#{options['scootx']}mm}}"
520 fig(name,'',{'raw'=>true})
523 def fig(name,caption=nil,options={})
525 'anonymous'=>'default',# true means figure has no figure number, but still gets labeled (which is, e.g.,
526 # necessary for photo credits)
527 # default is false, except if caption is a null string, in which case it defaults to true
528 'width'=>'narrow', # 'narrow'=52 mm, 'wide'=113 mm, 'fullpage'=171 mm
529 # refers to graphic, not graphic plus caption (which is greater for sidecaption option)
530 # may get automagically changed for 2-column layout
531 'width2'=>'auto', # width for 2-col layout;
532 # width='narrow', width2='auto' -- narrow figure stays same width, is not as wide as text colum
533 # width='fullpage',width2='auto' -- nothing special
534 # width='wide', width2='auto' -- makes it a sidecaption regardless of whether sidecaption was actually set
535 # width2='column' -- generates a warning if an explicitly created 82.5-mm wide figure doesn't exist
536 # width2='column_auto' -- like column, but expands automatically, and warns if an explicit alternative *does* exist
537 'sidecaption'=>false,
538 'sidepos'=>'t', # positioning of the side caption relative to the figure; can also be b, c
539 'float'=>'default', # defaults to false for narrow, true for wide or fullpage (because I couldn't get odd-even to work reliably for those if not floating)
540 'floatpos'=>'h', # standard latex positioning parameter for floating figures
541 'narrowfigwidecaption'=>false, # currently only supported with float and !anonymous
542 'suffix'=>'', # for use when a figure is used in more than one place, and we need to make the label unique;
543 # typically 'suffix'=>'2'; don't need this option on the first fig, only the second
544 'text'=>nil, # if it exists, puts the text in the figure rather than a graphic (name is still required for labeling)
545 # see macros \starttextfig and \finishtextfig
546 'raw'=>false, # used for anonymous inline figures, e.g., check marks; generates a raw call to includegraphics
547 'textbox'=>false # marginbox(), as used in Fund.
548 # not yet implemeted:
550 # or just have the script autodetect whether a translated version exists!
552 # figure is to be used in table of contents
553 # see macros \figureintoc, \figureintocnoresize
555 # figure in toc is to be used in the middle of a chapter (only allowed with toc=true)
556 # see macro figureintocscootx
558 # distance by which to scoot it down (only allowed with toc=true)
559 # see macro figureintocscooty
561 # automagically add a gray background
563 # automagically add a gray background if it's 2-column
565 # see macros \fignoresize, \inlinefignocaptionnoresize
567 caption.gsub!(/\A\s+/,'') # blank lines on the front make latex freak out
568 if caption=='' then caption=nil end
569 default_options.each {
571 if options[option]==nil then
572 options[option]=default
575 width=options['width']
576 if options['narrowfigwidecaption'] then
577 options['width']='wide'; options['sidecaption']=true; options['float']=false; options['anonymous']=false
579 if options['float']=='default' then
580 options['float']=(width=='wide' or width=='fullpage')
582 if options['anonymous']=='default' then
583 options['anonymous']=(!caption)
585 dir = find_directory_where_figure_is(name)
586 if dir.nil? && options['text'].nil? then save_complaint("figure #{name} not found in #{dir()}/figs, #{shared_figs()[0]}, or #{shared_figs()[1]}") end
587 #------------------------------------------------------------
588 if is_print then fig_print(name,caption,options,dir) end
589 #------------------------------------------------------------
590 if is_web then process_fig_web(name,caption,options) end
593 def process_fig_web(name,caption,options)
594 if options['raw'] then print "\\anonymousinlinefig{#{dir}/#{name}}"; return end
595 if caption==nil then caption='' end
596 # remove comments now, will be too late to do it later; can't use lookbehind because eruby compiled with ruby 1.8
597 caption.gsub!(/\\%/,'PROTECTPERCENT')
598 caption.gsub!(/%[^\n]*\n?/,' ')
599 caption.gsub!(/PROTECTPERCENT/,"\\%")
600 caption.gsub!(/\n/,' ')
601 text = options['text']
603 anon = '1' if options['anonymous']
605 if options['width']=='wide' then print "\n" end # kludgy fix for problem with html translator
606 print "#{$web_command_marker}fig,#{name},#{options['width']},#{anon},#{caption}END_CAPTION\n"
609 print "#{text}\n\n#{caption}\n\n" # bug ------------- not really correct
613 # sets $page_rendered_on as a side-effect (or sets it to nil if all.pos isn't available yet)
614 def fig_print(name,caption,options,dir)
615 if options['raw'] then spit("\\includegraphics{#{dir}/#{name}}"); return end
616 width=options['width']
618 sidepos = options['sidepos']
619 floatpos = options['floatpos']
620 suffix = options['suffix']
621 if (!(suffix=='')) and width=='wide' and ! options['float'] then die(name,"suffix not implemented for wide, !float") end
622 if (!(suffix=='')) and width=='narrow' and options['anonymous'] then die(name,"suffix not implemented for narrow, anonymous") end
624 #============================================================================
625 if $reuse.has_key?(name)
631 File.open($marg_file,'a') do |f|
632 f.print "fig:#{name},nmarg=#{$n_marg},ch=#{$ch}\n"
635 # Warn about figures that aren't floating, but that occur on a different page than the one on which they were invoked.
636 # Since the bug I'm trying to track down is a bug with marginal figures, only check if it's a marginal figure.
637 # This is somewhat inefficient.
638 if $in_marg and ! options['float'] then
639 invoked = $page_invoked_from[$n_marg]
640 $page_rendered_on=nil
641 last_l,last_page = nil,nil
642 if File.exist?(pos_file()) and !(invoked==nil) then
643 File.open(pos_file(),'r') do |f|
646 if line=~/^fig,label=fig:(.*),page=(.*),x=(.*),y=(.*),at=(.*)/ then
648 if l==name and !(last_l==l and last_page==page) then # second clause is because we get several lines in a row for each fig
649 $page_rendered_on=page if reuse==$reuse[name]
652 last_l,last_page = l,page
657 if !($page_rendered_on==nil) and !(invoked==nil) and !(invoked==$page_rendered_on) then
658 $stderr.print "***** warning: invoked on page #{invoked}, but rendered on page #{$page_rendered_on}, off by #{$page_rendered_on-invoked}, #{name}, ch.=#{$ch}\n" +
659 " This typically happens when the last few lines of the paragraph above the figure fall at the top of a page.\n"
662 #============================================================================
663 #----------------------- text ----------------------
664 if options['text']!=nil then
665 if options['textbox'] then
666 spit("\\startmargintextbox{#{name}}{#{caption}}\n#{options['text']}\n\\finishmargintextbox{#{name}}\n")
668 spit("\\starttextfig{#{name}}#{options['text']}\n\\finishtextfig{#{name}}{%\n#{caption}}\n")
671 #----------------------- narrow ----------------------
672 if width=='narrow' and options['text']==nil then
673 if options['anonymous'] then
675 spit("\\anonymousfig{#{name}}{%\n#{caption}}{#{dir}}\n")
677 spit("\\fignocaption{#{name}}{#{dir}}\n")
681 spit("\\fig{#{name}}{%\n#{caption}}{#{suffix}}{#{dir}}\n")
683 die(name,"no caption, but not anonymous")
687 #----------------------- wide ------------------------
688 if width=='wide' and options['text']==nil then
689 if options['anonymous'] then
690 if options['narrowfigwidecaption'] then die(name,'narrowfigwidecaption requires anonymous=false, and float=false') end
691 if options['float'] then
692 if caption || true then # see einstein-train
693 if options['sidecaption'] then
694 spit("\\widefigsidecaption{#{sidepos}}{#{name}}{%\n#{caption}}{anonymous}{#{floatpos}}{float}{#{suffix}}{#{dir}}\n")
696 spit("\\widefig[#{floatpos}]{#{name}}{%\n#{caption}}{#{suffix}}{anonymous}{#{dir}}\n")
699 die(name,"widefignocaption is currently only implemented as a nonfloating figure")
703 #die(name,"widefig is currently only implemented as a floating figure, because I couldn't get it to work right unless it was floating (see comments in lmcommon.sty)")
705 spit("\\widefignocaptionnofloat[#{dir}]{#{name}}\n")
709 if options['float'] then
710 if options['narrowfigwidecaption'] then die(name,'narrowfigwidecaption requires anonymous=false, and float=false') end
712 if options['sidecaption'] then
713 spit("\\widefigsidecaption{#{sidepos}}{#{name}}{%\n#{caption}}{labeled}{#{floatpos}}{float}{#{suffix}}{#{dir}}\n")
715 spit("\\widefig[#{floatpos}]{#{name}}{%\n#{caption}}{#{suffix}}{labeled}{#{dir}}\n")
718 die(name,"no caption, but not anonymous")
721 if options['narrowfigwidecaption'] then
722 spit("\\narrowfigwidecaptionnofloat{#{name}}{%\n#{caption}}{#{dir}}\n")
724 die(name,"The only wide figure that's implemented the option of not floating is narrowfigwidecaption. See comments in lmcommon.sty for explanation.")
729 #----------------------- fullpage ----------------------
730 if width=='fullpage' and options['text']==nil then
731 if options['anonymous'] then
733 die(name,"the combination of options fullpage+anonymous+caption is not currently supported")
735 spit("\\fullpagewidthfignocaption[#{dir}]{#{name}}\n")
739 spit("\\fullpagewidthfig[#{dir}]{#{name}}{%\n#{caption}}\n")
741 die(name,"no caption, but not anonymous")
745 #============================================================================
747 # Kludge: when figure is like ../../../foo/bar/baz, label includes the .. stuff; make a valid label.
748 # A better way to do this would be to have the macros never generate a label, and have the following
749 # be the only way a label is ever generated.
750 if name=~/\/([^\/]+)$/ then
751 spit("\\label{fig:#{$1}}")
754 die(name,"not handled")
763 # use fatal_error if not directly related to a figure
764 def die(name,message)
765 $stderr.print "eruby_util: figure #{name}, #{message}\n"
769 def self_check(label,text)
770 text.gsub!(/\n+\Z/) {""} # strip excess newlines at the end
771 text.gsub!(/\\\\/) {"\\"} # double backslashes to single; this is just a shortcut because I screwed up and unnecessarily changed a bunch of \ to \\
772 print "\\begin{selfcheck}{#{label}}\n#{text}\n\\end{selfcheck}\n"
773 write_to_answer_data('self_check',label)
776 def read_whole_file(file)
778 File.open(file,'r') { |f|
779 x = f.gets(nil) # nil means read whole file
784 #--------------------------------------------------
785 # code for numbering style used in Fundamentals of Calculus
786 #--------------------------------------------------
789 return $config['hw_block_style']==1 # set in book.config
792 # first block is 0<->a
793 def integer_to_base24(i)
794 if i<0 then fatal_error("negative i in integer_to_base24") end
795 if i<24 then return "abcdefghijkmnpqrstuvwxyz"[i] end
796 return integer_to_base24(i/24)+integer_to_base24(i%24)
799 def base24_to_integer(s)
800 if s.length==0 then fatal_error("null string in base24_to_integer") end
802 i = "abcdefghijkmnpqrstuvwxyz".index(s)
803 if i.nil? then fatal_error("illegal character #{s} in base24_to_integer") end
806 return base24_to_integer(s[0,s.length-1])*24+base24_to_integer(s[s.length-1])
809 # test integer_to_base24() and base24_to_integer()
811 [[0,'a'],[1,'b'],[23,'z'],[24,'ba']].each { |x|
814 unless integer_to_base24(i)==s then
815 $stderr.print "integer_to_base24("+i.to_s+") gives "+integer_to_base24(i)+", should have given "+s+"\n"
818 unless base24_to_integer(s)==i then
819 $stderr.print "base24_to_integer("+s+") gives "+base24_to_integer(s).to_s+", should have given "+i+"\n"
826 if $hw_freeze<0 then fatal_error("hw_freeze invoked, and $hw_freeze already less than 0? ") end
827 $hw_freeze = $hw_freeze+1
831 $hw_freeze = $hw_freeze-1
832 if $hw_freeze<0 then fatal_error("hw_end_freeze invoked, and $hw_freeze already 0? ") end
836 return integer_to_base24($hw_block)
840 label = $hw_number.to_s
841 if hw_block_style() then label = get_hw_block+$hw_number_in_block.to_s end
845 # control of letter that labels block
846 # hw_block ... bumps by 3
847 # hw_block(1) ... bumps by 1
848 # hw_block('b') ... sets it to 'b'
851 $hw_number_in_block = 0
852 print %q~\vspace{\stretch{2}}~
853 # ... twice as big as what's at the end of homeworkforcelabel in lmcommon.sty
854 if x.nil? then $hw_block = $hw_block+3; return end
855 if x.class() == Fixnum then $hw_block = $hw_block+x; return end
856 if x.class() == String then $hw_block = base24_to_integer(x); return end
857 fatal_error("error in hw_block, arg has class=#{x.class()}")
860 #--------------------------------------------------
862 def hw(name,options={},difficulty=1) # used in Fundamentals of Calculus, which has all hw in chNN/hw; other books use begin_hw
863 if difficulty==nil then difficulty=1 end
864 begin_hw(name,difficulty,options)
865 x = read_whole_file("ch#{$ch}/hw/#{name}.tex")
866 print x.sub(/\n+\Z/,'')+"\n" # exactly one newline at end before \end{homework}
867 if options['solution'] then hw_solution() end
871 def begin_hw(name,difficulty=1,options={})
872 if difficulty==nil then difficulty=1 end
873 if calc() then options['calc']=false end
875 if options['calc'] then calc='1' end
877 $hw_number_in_block += 1
878 $hw[$hw_number] = name
879 $hw_has_solution[$hw_number] = false
881 $store_hw_label[$hw_number] = label
882 print "\\begin{homeworkforcelabel}{#{name}}{#{difficulty}}{#{calc}}{#{label}}"
886 $hw_has_solution[$hw_number] = true # for problems.csv
888 write_to_answer_data('answer')
892 print "\\hwhint{hwhint:#{label}}"
893 write_to_answer_data('hint')
897 print "\\hwans{hwans:#{$hw[$hw_number]}}"
898 write_to_answer_data('bare_answer')
901 $answer_data_file = 'answers.csv'
903 $answer_text = {'answer'=>{},'hint'=>{},'self_check'=>{},'bare_answer'=>{}}
904 $answer_long_label_to_short = {}
906 def clear_answer_data
907 File.open($answer_data_file,'w') { |f| }
910 def write_to_answer_data(type,label=nil)
911 if label==nil then label = $hw[$hw_number] end
912 File.open($answer_data_file,'a') { |f|
913 f.print "#{$ch.to_i},#{label},#{type}\n"
917 def read_answer_data()
919 if ! File.exist?($answer_data_file) then return end
920 File.open($answer_data_file,'r') { |f|
921 a = f.gets(nil) # nil means read whole file
922 a.scan(/(\d+),(.*),(.*)/) { |ch,name,type|
923 $answer_data.push([ch.to_i,name,type])
928 def print_general_answer_section_header(header)
929 print_end_matter_section_header(header)
932 def print_photo_credits_section_header(header)
933 print_end_matter_section_header(header)
936 def print_end_matter_section_header(header)
937 header = alter_titlecase(header,0)
938 print "\\addcontentsline{toc}{section}{#{header}}\\formatlikechapter{#{header}}\\\\*\n\n"
941 def print_answers_of_one_type(lo_ch,hi_ch,type,header)
942 #$stderr.print "print_answers_of_one_type, type=#{type}\n"
944 print "\\hwanssection{#{header}}\n\n"
946 for ch in lo_ch..hi_ch do
947 $answer_data.each { |a|
948 #$stderr.print "type=",type,' ',a.join(','),"\n" if ch==0 && a[0]==0
950 if ch==a[0] && type==a[2] then
952 describe_them = {'answer'=>'Solutions','hint'=>'Hints','self_check'=>'Answers to self-checks','bare_answer'=>'Answers'}[type]
953 print '\par\pagebreak[3]\vspace{2mm}\noindent\formatlikesubsection{'+describe_them+' for chapter '+ch.to_s+'}\\\\*'
956 long = answer_short_label_to_long(name,type)
958 save_complaint("No answer text available for problem #{name}, type #{type}")
960 print answer_header(long,type)+$answer_text[type][long]
967 def answer_short_label_to_long(short,type)
968 $answer_long_label_to_short.each { |long,s|
969 if s==short and !$answer_text[type][long].nil? then return long end
974 def answer_header(label,type)
975 macro = {'answer'=>'hwsolnhdr','hint'=>'hinthdr','self_check'=>'scanshdr','bare_answer'=>'hwanshdr'}[type]
976 if macro==nil then fatal_error("error in eruby_util.rb, answer_header(), illegal type: #{type}") end
977 short_label = $answer_long_label_to_short[label]
978 return "\\#{macro}{#{short_label}}\\\\*\n"
981 def list_some_problems(names)
984 a.push("p.~\\pageref{hw:#{name}}, \\#\\ref{hw:#{name}}")
990 print "\\ref{hw:#{name}}"
991 $hw_names_referred_to.push(name)
995 print "\\end{homeworkforcelabel}"
998 def hint_text(label,text=nil)
999 set_answer_text(label,"hint-"+label,text,'hint')
1002 def self_check_answer(label,text=nil)
1003 set_answer_text(label,nil,text,'self_check')
1006 def bare_answer(label,text=nil)
1007 set_answer_text(label,nil,text,'bare_answer')
1010 def answer(label,text=nil) # don't call this directly
1011 set_answer_text(label,nil,text,'answer')
1014 def set_answer_text(label,long_label,text,type)
1015 # long_label is because some problems may have both hint and answer, etc.
1016 # short label is used only for latex references
1017 if long_label==nil then long_label = label end
1018 text = handle_answer_text_caching(long_label,text,type)
1019 $answer_text[type][long_label] = text
1020 $answer_long_label_to_short[long_label] = label
1023 def handle_answer_text_caching(label,text,type)
1024 file = File.expand_path("../share/answers") + "/" + label + ".tex"
1025 have_file = FileTest.exist?(file)
1026 gave_text = text!=nil
1027 if gave_text && ! have_file then
1028 File.open(file,'w') { |f|
1032 if gave_text && have_file then
1033 # so I can cut and paste to replace the old version that includes the text with the new version that doesn't
1034 func = {'answer'=>"answer",'hint'=>'hint_text','self_check'=>'self_check_answer','bare_answer'=>'bare_answer'}[type]
1035 $stderr.print "<% #{func}(\"#{label}\") %>\n"
1037 if !gave_text && ! have_file then
1038 $stderr.print "error in eruby_util.rb, handle_answer_text_caching: file #{file} doesn't exist\n"
1042 File.open(file,'r') { |f|
1043 text = f.gets(nil) # nil means slurp whole file
1045 text.gsub!(/\n+\Z/) {"\n\n"} # exactly two newlines at the end
1050 def part_title(title)
1051 title = alter_titlecase(title,-1)
1052 print "\\mypart{#{title}}"
1055 def begin_ex(title,label='',columns=1)
1056 title = alter_titlecase(title,1)
1057 column_command = (columns==1 ? "\\onecolumn" : "\\twocolumn");
1058 print "\\begin{handson}{#{label}}{#{title}}{#{column_command}}"
1062 print "\\end{handson}"
1067 # end_sec('spacetime-interval') ... try to use this form, which acts as a check on whether the hierarchy
1068 # of sections is out of whack
1069 # It's OK if begin_sec() gives a label but end_sec() doesn't.
1070 def end_sec(label='')
1072 $count_section_commands += 1
1074 if debug then $stderr.print (' '*$section_level)+"end_sec('#{label}')\n" end
1075 began_sec = $section_label_stack.pop # is '' if the section was unlabeled, nil if the stack was empty
1076 began_title = $section_title_stack.pop
1077 if began_sec.nil? then fatal_error("end_sec('#{label}') occurs without any begin_sec") end
1078 if label!=began_sec and !(began_sec!='' and label=='') then
1079 fatal_error("mismatch between labels, begin_sec(\"#{began_title}\",...,'#{began_sec}') and end_sec('#{label}')")
1083 # example of use: begin_sec("The spacetime interval",nil,'spacetime-interval',{'optional'=>true})
1084 # In this example, the LaTeX label might be sec:spacetime-interval in LM, subsec:spacetime-interval in SN.
1085 # The corresponding end_sec could use an optional arg, end_sec('spacetime-interval'), which helps to make
1086 # sure the hierarchical structure doesn't get out of whack. The structure tends to get out of whack when
1087 # different books share text, using m4 conditionals.
1088 def begin_sec(title,pagebreak=nil,label='',options={})
1090 $count_section_commands += 1
1091 if debug then $stderr.print (' '*$section_level)+"begin_sec(\"#{title}\",#{pagebreak},\"#{label}\")\n" end
1093 $section_label_stack.push(label) # if not labeled, then label is ''
1094 $section_title_stack.push(title)
1095 # In LM, section level 1=section, 2=subsection, 3=subsubsection; 0 would be chapter, but chapters aren't done with begin_sec()
1096 if $section_level==0 || $section_level>4 then
1098 if $section_level==0 then e='zero section level (happens in NP Preface)' end
1099 if $section_level>3 then e='section level is too deep' end
1100 $stderr.print "warning, at #{$count_section_commands}th section command, ch #{$ch}, section #{title}, section level=#{$section_level}, #{e}\n"
1103 # Guard against the easy error of writing begin_sec("title","label"), leaving out pagebreak.
1104 unless pagebreak.nil? or pagebreak.kind_of?(Integer) then fatal_error("begin_sec(\"#{title}\",\"#{pagebreak}\",...) has non-integer second argument; did you leave out the pagebreak parameter?") end
1105 if pagebreak==nil then pagebreak=4-$section_level end
1106 if pagebreak>4 then pagebreak=4 end
1107 if pagebreak<0 then pagebreak=0 end
1108 pagebreak = '['+pagebreak.to_s+']'
1109 if $section_level>=3 then pagebreak = '' end
1112 if calc() then options['calc']=false end
1113 if $section_level==1 then
1114 if options['calc'] and options['optional'] then macro='myoptionalcalcsection' end
1115 if options['calc'] and !options['optional'] then macro='mycalcsection' end
1116 if !options['calc'] and options['optional'] then macro='myoptionalsection' end
1117 if !options['calc'] and !options['optional'] then macro='mysection' end
1120 if $section_level==2 then
1121 if options['toc']==false then
1122 macro = 'mysubsectionnotoc'
1124 if options['optional'] then macro='myoptionalsubsection' else macro = 'mysubsection' end
1126 label_level = 'subsec'
1128 if $section_level==3 then
1129 macro = 'subsubsection'
1130 label_level = 'subsubsec'
1132 if $section_level==4 then
1133 macro = 'myssssection'
1134 label_level = 'subsubsubsec'
1136 title = alter_titlecase(title,$section_level)
1137 cmd = "\\#{macro}#{pagebreak}{#{title}}"
1138 t = sectioning_command_with_href(cmd,$section_level,label,label_level,title)
1141 $section_most_recently_begun = title
1142 #$stderr.print "in begin_sec(), eruby_util.rb: level=#{$section_level}, title=#{title}, macro=#{macro}\n"
1145 def begin_hw_sec(title='Problems')
1146 label = "hw-#{$ch}-#{title.downcase.gsub(/\s+/,'_')}"
1148 \\begin{hwsection}[#{title}]
1149 \\anchor{anchor-#{label}}% navigator_package
1152 t = t + "\\addcontentsline{toc}{section}{#{title}}"
1154 t = t + "\\addcontentsline{toc}{section}{\\protect\\link{#{label}}{#{title}}}"
1160 print '\end{hwsection}'
1163 def sectioning_command_with_href(cmd,section_level,label,label_level,title)
1164 # http://tex.stackexchange.com/a/200940/6853
1165 name_level = {0=>'chapter',1=>'section',2=>'subsection',3=>'subsubsection',4=>'subsubsubsection'}[section_level]
1170 #label = ("ch-"+$ch.to_s+"-"+name_level+"-"+title).downcase.gsub(/[^a-z]/,'-').gsub(/\-\-+/,'-')
1171 #label = label + rand(10000).to_s + (Time::new.to_i % 10000).to_s # otherwise I get some non-unique ones
1172 label = "ch-#{$ch}-#{$label_counter}"
1175 if label != '' then # shouldn't happen, since we construct one above if need be
1176 complete_label = "#{label_level}:#{label}"
1177 label_command="\\label{#{complete_label}}"
1178 anchor_command = "\\anchor{anchor-#{complete_label}}" # navigator_package
1180 anchor_command_1 = ''
1181 anchor_command_2 = ''
1182 if section_level==0 then anchor_command_2=anchor_command else anchor_command_1=anchor_command end
1183 if is_prepress then toc_macro="toclinewithoutlink" else toc_macro="toclinewithlink" end
1184 # In the following, I had been using begingroup/endgroup to temporarily disable \addcontentsline,
1185 # but that had the side-effect that had the side effect of causing a \label{} that came after
1186 # begin_sec to have a null string as the label instead of the section number.
1188 # similar code in begin_hw_sec
1192 \\let\\oldacl\\addcontentsline
1193 \\renewcommand{\\addcontentsline}[3]{}% temporarily disable \\addcontentsline
1194 #{anchor_command_1}#{cmd}#{label_command}#{anchor_command_2}
1196 \\let\\addcontentsline\\oldacl
1197 \\#{toc_macro}{#{name_level}}{#{complete_label}}{#{title}}{\\the#{name_level}}
1203 return ENV['PREPRESS']=='1'
1206 # The following allows me to control what's titlecase and what's not, simply by changing book.config. Since text can be shared between books,
1207 # and the same title may be a section in LM but a subsection in SN, this needs to be done on the fly.
1208 def alter_titlecase(title,section_level)
1209 if section_level>=$config['titlecase_above'] then
1210 return remove_titlecase(title)
1212 return add_titlecase(title)
1216 def add_titlecase(title)
1219 # Current-conducting -> Current-Conducting
1220 foo.gsub!(/(?<![\w'"`{}\\$])(\w)/) {$1.upcase} # Change every initial letter to uppercase. Handle Bob's, Schr\"odinger, Amp\`{e}re's
1221 [ 'a','the','and','or','if','for','of','on','by' ].each { |tiny| # Change specific short words back to lowercase.
1222 foo.gsub!(/(?<!\w)#{tiny}(?!\w)/i) {tiny}
1224 foo = initial_cap(foo) # Make sure initial word ends up capitalized.
1225 acronyms_and_symbols_uppercase(foo) # E.g., FWHM.
1226 #if title != foo then $stderr.print "changing title from #{title} to #{foo}\n" end
1230 $read_proper_nouns = false
1233 if !$read_proper_nouns then
1234 json_file = whichever_file_exists(["../scripts/proper_nouns.json","scripts/proper_nouns.json"])
1236 File.open(json_file,'r') { |f| json_data = f.gets(nil) }
1237 if json_data == '' then $stderr.print "Error reading file #{json_file} in eruby_util.rb"; exit(-1) end
1238 $proper_nouns = JSON.parse(json_data)
1239 $read_proper_nouns = true
1241 return $proper_nouns
1244 def remove_titlecase(title)
1246 foo = initial_cap(foo.downcase) # first letter is capital, everything after that lowercase
1247 # restore caps on proper nouns:
1248 proper_nouns()["noun"].each { |proper|
1249 foo.gsub!(/(?<!\w)#{Regexp::quote(proper)}/i) {|x| initial_cap(x)}
1250 # ... the negative lookbehind prevents, e.g., damped and example from becoming DAmped and ExAmple
1251 # If I had a word like "amplification" in a title, I'd need to special-case that below and change it back.
1253 foo.gsub!(/or Machines/,"or machines") # LM 4.4 (Ernst Mach)
1254 foo.gsub!(/motion Machine/,"motion machine") # LM 10 (Ernst Mach)
1255 foo.gsub!(/simple Machines/,"simple machines") # LM 8.3 (Ernst Mach)
1256 foo.gsub!(/e=mc/,"E=mc") # LM 25
1257 foo.gsub!(/ke=/,"KE=") # Mechanics 12.4
1258 foo.gsub!(/k=/,"K=") # Mechanics 12.4; in case I switch from KE to K
1259 foo.gsub!(/l'h/,"L'H") # L'Hopital; software isn't smart enough to handle apostrophe and housetop accent
1260 foo.gsub!(/L'h/,"L'H")
1261 foo.gsub!(/ i /," I ")
1262 # logic above can't handle multi-word patterns
1263 proper_nouns()["multiword"].each { |proper| # e.g., proper="Big Bang"
1264 foo.gsub!(/#{Regexp::quote(proper.downcase)}/) {proper}
1266 acronyms_and_symbols_uppercase(foo) # e.g., FWHM
1267 #if title != foo then $stderr.print "changing title from #{title} to #{foo}\n" end
1271 def acronyms_and_symbols_uppercase(foo)
1272 # Acronyms and symbols that need to be uppercase no matter what:
1273 proper_nouns()["acronym"].each { |a| # e.g., a="FWHM"
1274 foo.gsub!(/(?<!\w)(#{Regexp::quote(a.downcase)})(?!\w)/) {$1.upcase}
1279 # Note that we have some subsections like "2. The medium is not transported with the wave.", where the initial cap is not the first character.
1280 # These are handled correctly because it's sub(), not gsub(), so it just changes the first a-zA-Z character.
1281 # The A-Z case is the one where it's already got an initial cap (e.g., don't want to end up with "HEllo".
1282 return x.sub(/([a-zA-Z])/) {|x| x.upcase}
1287 if $section_level != -1 then
1288 $stderr.print "warning, at end_chapter, ch #{$ch}, section level at end of chapter is #{$section_level}, should be -1; probably begin_sec's and end_sec's are not properly balanced (happens in NP preface)\n"
1290 $hw_names_referred_to.each { |name|
1291 $stderr.print "hwref:#{name}\n"
1293 File.open("ch#{$ch}_problems.csv",'w') { |f|
1297 if $ch=='002' then chnum=0 end
1298 1.upto($hw_number) { |i| # output doesn't always get sorted correctly; see fund/solns/prep_solutions for perl code that sorts it correctly
1300 f.print "#{book},#{chnum},#{$store_hw_label[i]},#{name},#{$hw_has_solution[i]?'1':'0'}\n"
1303 mv = whichever_file_exists(['mv_silent','../mv_silent'])
1304 print "\n\\write18{#{mv} all.pos ch#{$ch}.pos}\n"
1307 def whichever_file_exists(files)
1309 if File.exist?(f) then return f end
1311 $stderr.print "Error in eruby_util.rb, whichever_file_exists(#{files.join(',')}): none of these files exist. Current working dir is #{Dir.pwd}\n"
1315 def code_listing(filename,code)
1317 $n_code_listing = $n_code_listing+1
1318 File.open("code_listing_ch#{$ch}_#{$n_code_listing}_#{filename}",'w') { |f|
1323 def chapter(number,title,label,caption='',options={})
1326 'anonymous'=>'default',# true means figure has no figure number, but still gets labeled (which is, e.g., necessary for photo credits)
1327 # default is false, except if caption is a null string, in which case it defaults to true
1328 'width'=>'wide', # 'wide'=113 mm, 'fullpage'=171 mm
1329 # refers to graphic, not graphic plus caption (which is greater for sidecaption option)
1330 'sidecaption'=>false,
1331 'special_width'=>nil, # used in CL4, to let part of the figure hang out into the margin
1332 'short_title'=>nil, # used in TOC; if omitted, taken from title
1333 'very_short_title'=>nil # used in running headers; if omitted, taken from short_title
1338 default_options.each {
1340 if options[option]==nil then
1341 options[option]=default
1344 if options['short_title']==nil then options['short_title']=title end
1345 if options['very_short_title']==nil then options['very_short_title']=options['short_title'] end
1346 opener = options['opener']
1348 if !figure_exists_in_my_own_dir?(opener) then
1349 # bug: doesn't support \figprefix
1350 # ! LaTeX Error: File `ch02/figs/../9share/mechanics/figs/pool' not found.
1353 unless (File.exist?("#{dir}/#{opener}.pdf") or File.exist?("#{dir}/#{opener}.jpg") or File.exist?("#{dir}/#{opener}.png")) then
1356 options['opener']="../../#{dir}/#{opener}"
1358 if options['anonymous']=='default' then
1359 options['anonymous']=(caption=='')
1362 title = alter_titlecase(title,0)
1363 if is_print then chapter_print(number,title,label,caption,options) end
1364 if is_web then chapter_web(number,title,label,caption,options) end
1367 def chapter_web(number,title,label,caption,options)
1368 if options['opener']!='' then
1369 process_fig_web(options['opener'],caption,options)
1371 print "\\mychapter{#{title}}\n"
1374 def chapter_print(number,title,label,caption,options)
1375 opener = options['opener']
1376 has_opener = (opener!='')
1378 bare_label = label.clone.gsub!(/ch:/,'')
1379 #$stderr.print "in chapter_print, bare_label=#{bare_label}\n"
1381 #append = "\\anchor{anchor-#{label}}" # navigator package
1382 File.open('brief-toc-new.tex','a') { |f|
1383 # LM and Me. don't use brief-toc-new.tex
1385 f.print "\\brieftocentry{#{label}}{#{title}} \\\\\n"
1387 f.print "\\brieftocentrywithlink{#{label}}{#{title}} \\\\\n"
1391 result = "\\mychapter{#{options['short_title']}}{#{options['very_short_title']}}{#{title}}#{append}"
1393 opener=~/([^\/]+)$/ # opener could be, e.g., ../../../9share/optics/figs/crepuscular-rays
1395 ol = "\\label{fig:#{opener_label}}" # needs label for figure credits, and TeX isn't smart enough to handle cases where it's got ../.., etc. on the front
1396 # not strictly correct, because label refers to chapter, but we only care about page number for photo credits
1397 if options['width']=='wide' then
1399 if !options['sidecaption'] then
1400 if options['special_width']==nil then
1401 result = "\\mychapterwithopener{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1403 result = "\\specialchapterwithopener{#{options['special_width']}}{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1406 if options['anonymous'] then
1407 result = "\\mychapterwithopenersidecaptionanon{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1409 result = "\\mychapterwithopenersidecaption{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1413 result = "\\mychapterwithopenernocaption{#{opener}}{#{title}}#{ol}#{append}"
1416 if options['anonymous'] then
1418 result = "\\mychapterwithfullpagewidthopener{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1420 result = "\\mychapterwithfullpagewidthopenernocaption{#{opener}}{#{title}}#{ol}#{append}"
1423 $stderr.print "********************************* ch #{ch}full page width chapter openers are only supported as anonymous figures ************************************\n"
1429 $stderr.print "**************************************** Error, ch #{$ch}, processing chapter header. ****************************************\n"
1432 print sectioning_command_with_href(result,0,bare_label,'ch',title)
1433 #print "#{result}\\label{#{label}}\n"
1438 def photo_credit(label,description,credit)
1439 $photo_credits.push([label,description,credit,'normal'])
1442 def toc_photo_credit(description,credit)
1443 $photo_credits.push(['',description,credit,'contents'])
1446 def pagenum_or_zero(label)
1447 if label == '' then return 0 end
1449 if $ref[l]==nil then return 0 else return $ref[l][1] end
1452 def print_photo_credits
1453 $photo_credits.sort{ |a,b| pagenum_or_zero(a[0]) <=> pagenum_or_zero(b[0]) }.each { |c|
1458 #print "label #{label}="
1459 #if $ref[label]!=nil then print $ref[label][1] end
1460 if type=='normal' then
1461 print "\\cred{#{label}}{#{description}}{#{credit}}\n"
1463 print "\\docred{Contents}{#{description}}{#{credit}}\n"
1466 #$ref.keys.each { |k| print k,$ref[k] }