add LM 5 hw, caryatid
[light-and-matter.git] / eruby_util.rb
blob89a2305f9820e1cae7afc62e8ea15933b6b1dbce
1 # (c) 2006-2013 Benjamin Crowell, GPL licensed
2
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
8 #         "make preflight".
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.
18 require 'json'
20 $label_counter = 0 # for generating labels when the user doesn't supply one
21 $n_code_listing = 0
22 $hw_number = 0
23 $hw_number_in_block = 0
24 $store_hw_label = []
25 $hw_block = 0 # for style used in Fundamentals of Calculus
26 $hw = []
27 $hw_has_solution = []
28 $hw_names_referred_to = []
29 $hw_freeze = 0
30 $tex_points_to_mm = (25.4)/(65536.*72.27)
31 $n_marg = 0
32 $in_marg = false
33 $geom_file = "geom.pos"
34 $checked_geom = false
35 $geom_exists = nil
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.
43 $checked_pos = false
44 $pos_exists = nil
45 $marg_file = "marg.pos"
46 $feedback = []
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 = []
52 $reuse = {}
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
57 $section_level = -1
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()
66   exit(-1)
67 end
69 def 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"
73   end
74   return result
75 end
77 def save_complaint(message)
78   File.open('eruby_complaints','a') { |f| 
79     f.print message,"\n"
80   }
81 end
83 # returns contents or nil on error; for more detailed error reporting, see slurp_file_with_detailed_error_reporting()
84 def slurp_file(file)
85   x = slurp_file_with_detailed_error_reporting(file)
86   return x[0]
87 end
89 # returns [contents,nil] normally [nil,error message] otherwise
90 def slurp_file_with_detailed_error_reporting(file)
91   begin
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
95       return [t,nil]
96     }
97   rescue
98     return [nil,"Error opening file #{file} for input: #{$!}."]
99   end
103 #--------------------------------------------------------------------------
104 config_file = 'book.config'
105 if ! File.exist?(config_file) then fatal_error("error, file #{config_file} does not exist") end
106 $config = {
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
116       value = value.to_i
117     end
118     $config[var] = value
119   }
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'
135 $ref = {}
136 n_defs = {}
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}")
145       end
146     end
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
150   }
152 avg = 0
153 n = 0
154 n_defs.keys.each {|fig|
155   n = n+1
156   avg = avg + n_defs[fig]
158 avg = avg.to_f / n
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
178 def calc
179   return ENV['CALC']=='1'
182 # set by run_eruby.pl
183 # for use when generating screen-resolution figures
184 # e.g., ../9share/optics
185 def shared_figs
186   return [ENV['SHARED_FIGS'],ENV['SHARED_FIGS2']]
189 def is_print
190   return ENV['BOOK_OUTPUT_FORMAT']!='web'
193 def is_web
194   return ENV['BOOK_OUTPUT_FORMAT']=='web'
197 def dir
198   return ENV['DIR']
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
205   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")
208   end
209   $conditional_stack.push(condition)
210   if !condition then
211     print "\n\\begin{comment}\n" # requires comment package; newlines before and after are required by that package
212   end
215 def end_if
216   condition = $conditional_stack.pop
217   if !condition then
218     print "\n\\end{comment}\n" # requires comment package; newlines before and after are required by that package
219   end
222 def pos_file
223   return "ch#{$ch}.pos"
226 def previous_pos_file
227   p = $ch.to_i-1
228   if p<0 then return nil end
229   if p<10 then p = '0'+p.to_s end
230   return "ch#{p}.pos"
233 # returns data in units of mm, in the coordinate system used by pdfsavepos (positive y up)
234 def geom(what)
235   if ! $checked_geom then
236     $geom_exists = File.exist?($geom_file)
237     if $geom_exists then
238       File.open($geom_file,'r') do |f|
239         line = f.gets
240         if !(line=~/pt/) then # make sure it's already been parsed into millimeters
241           $geom = line.split
242         end
243       end
244     end
245     $checked_geom = true
246   end
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
250   return result
253 def end_marg
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
257   $in_marg = false
260 def marg(delta_y=0)
261   if $in_marg then die('(marg)','marg, but already in a marg') end
262   $n_marg = $n_marg+1
263   $in_marg = true
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')
274     x=geom('oddfigminx')
275     y=maxy
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|
281         f.each_line { |line|
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
287           else
288             die(name,"syntax error in file #{fig_file}, line=#{line}")
289           end
290         }
291       end
292       File.delete(fig_file) # otherwise it grows by being appended to every time we run tex
293     end
294     if $feedback_exists then
295       feed = $feedback[$n_marg]
296       page = feed['page']
297       refy = feed['refy']
298       deltay = feed['deltay']
299       y = refy*$tex_points_to_mm+deltay
300       y_raw = y
301       debug = false
302       $stderr.print "miny=#{miny}\n" if debug
303       ht = height_of_marg
304       maxht = maxy-miny
305       if page%2==0 then
306         x=geom('evenfigminx') # left page
307       else
308         x=geom('oddfigminx') # right page
309       end
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
317       if !(ht==nil) then
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
320         if ht>maxht then
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.
323           fudge = ht-maxht
324           if fudge>max_fudge then fudge=max_fudge end
325           y=maxy+fudge
326         else
327           if y-ht<miny+tol_in then y=miny+ht end
328         end
329       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
342   marg(delta_y)
343   fig(name,caption,options)
344   end_marg
347 def mm(x)
348   if x==nil then return '' end
349   return sprintf((x+0.5).to_i.to_s,"%d")
352 # severities:
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.
363   mine = {}
364   if File.exist?($marg_file) then
365     File.open($marg_file,'r') do |f|
366       f.each_line { |line|
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)
370         else
371           $stderr.print "error in #{$marg_file}, no match?? #{line}\n"
372         end
373       }
374     end
375   end
376   if severity>1 then
377     $stderr.print "warning, severity #{severity} nmarg=#{nmarg}, ch. #{$ch}, p. #{page}, #{mine.keys.join(',')}: #{message}\n"
378   end
381 def pos_file_exists
382   if ! $checked_pos then
383     $pos_exists = File.exist?(pos_file())
384     $checked_pos = true
385   end
386   return $pos_exists
389 # returns height in mm, or nil if the all.pos file doesn't exist yet, or figure not listed in it
390 def height_of_marg
391   #debug = ($ch.to_i==0 and $n_marg==6)
392   debug = false
393   if debug then 
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`
396   end
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.
400   mine = Hash.new
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. ********************
409     f.each_line { |line|
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]
414       end
415     }
416   end
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
424   lo_y = huge
425   hi_y = -huge
426   found = false
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)
435   end
436   if !found then
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.
439   end
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
446   return height
449 def get_low_and_hi!(found,lo_y,hi_y,filename,mine)
450   File.open(filename,'r') do |f|
451     f.each_line { |line|
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
457           found = true
458         end
459       end
460     }
461   end
462   [found,lo_y,hi_y]
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
477   s = shared_figs()
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
480   return nil
483 def figure_in_toc(name,options={})
484   default_options = {
485     'scootx'=>0,
486     'scooty'=>0,
487     'noresize'=>false
488   }
489   default_options.each { 
490     |option,default|
491     if options[option]==nil then
492       options[option]=default
493     end
494   }
495   d = 'ch00/figs'
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
498     d='../share/toc' 
499   end
500   if options['noresize'] then
501     print "\\addtocontents{toc}{\\protect\\figureintocnoresize{#{d}/toc-#{name}}}"
502   else
503     if options['scootx']==0 then
504       if options['scooty']==0 then
505         print "\\addtocontents{toc}{\\protect\\figureintoc{#{d}/toc-#{name}}}"
506       else
507         print "\\addtocontents{toc}{\\protect\\figureintocscooty{#{d}/toc-#{name}}{#{options['scooty']}mm}}"
508       end
509     else
510       print "\\addtocontents{toc}{\\protect\\figureintocscootx{#{d}/toc-#{name}}{#{options['scootx']}mm}}"
511     end
512   end
515 def x_mark
516   raw_fig('x-mark')
519 def raw_fig(name)
520   fig(name,'',{'raw'=>true})
523 def fig(name,caption=nil,options={})
524   default_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: 
549     #    translated=false
550     #      or just have the script autodetect whether a translated version exists!
551     #    toc=false
552     #      figure is to be used in table of contents
553     #      see macros \figureintoc, \figureintocnoresize
554     #    midtoc=false
555     #      figure in toc is to be used in the middle of a chapter (only allowed with toc=true)
556     #      see macro figureintocscootx
557     #    scootdown=0
558     #      distance by which to scoot it down (only allowed with toc=true)
559     #      see macro figureintocscooty
560     #    gray=false
561     #      automagically add a gray background
562     #    gray2=false
563     #      automagically add a gray background if it's 2-column
564     #    resize=true
565     #      see macros \fignoresize, \inlinefignocaptionnoresize
566   }
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 { 
570     |option,default|
571     if options[option]==nil then
572       options[option]=default
573     end
574   }
575   width=options['width']
576   if options['narrowfigwidecaption'] then
577     options['width']='wide'; options['sidecaption']=true; options['float']=false; options['anonymous']=false
578   end
579   if options['float']=='default' then
580     options['float']=(width=='wide' or width=='fullpage')
581   end
582   if options['anonymous']=='default' then
583     options['anonymous']=(!caption)
584   end
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']
602   anon = '0'
603   anon = '1' if options['anonymous']
604   if text==nil then
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"
607   else
608     text.gsub!(/\n/,' ')
609     print "#{text}\n\n#{caption}\n\n" # bug ------------- not really correct
610   end
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']
617   $fig_handled = false
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
623   print "\\noindent"
624   #============================================================================
625   if $reuse.has_key?(name)
626     $reuse[name]+=1
627   else
628     $reuse[name]=0
629   end
630   if $in_marg then
631     File.open($marg_file,'a') do |f|
632       f.print "fig:#{name},nmarg=#{$n_marg},ch=#{$ch}\n"
633     end
634   end
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|
644         reuse = 0
645         f.each_line { |line|
646           if line=~/^fig,label=fig:(.*),page=(.*),x=(.*),y=(.*),at=(.*)/ then
647             l,page=$1,$2.to_i
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]
650               reuse+=1
651             end
652             last_l,last_page = l,page
653           end
654         }
655       end
656     end
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"
660     end
661   end
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")
667     else
668       spit("\\starttextfig{#{name}}#{options['text']}\n\\finishtextfig{#{name}}{%\n#{caption}}\n")
669     end
670   end
671   #----------------------- narrow ----------------------
672   if width=='narrow' and options['text']==nil then
673     if options['anonymous'] then
674       if caption then
675         spit("\\anonymousfig{#{name}}{%\n#{caption}}{#{dir}}\n")
676       else
677         spit("\\fignocaption{#{name}}{#{dir}}\n")
678       end
679     else # not anonymous
680       if caption then
681         spit("\\fig{#{name}}{%\n#{caption}}{#{suffix}}{#{dir}}\n")
682       else
683         die(name,"no caption, but not anonymous")
684       end
685     end
686   end
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")
695           else
696             spit("\\widefig[#{floatpos}]{#{name}}{%\n#{caption}}{#{suffix}}{anonymous}{#{dir}}\n")
697           end
698         else
699           die(name,"widefignocaption is currently only implemented as a nonfloating figure")
700         end
701       else # not floating
702         if caption then
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)")
704         else
705           spit("\\widefignocaptionnofloat[#{dir}]{#{name}}\n")
706         end
707       end
708     else # not anonymous
709       if options['float'] then
710         if options['narrowfigwidecaption'] then die(name,'narrowfigwidecaption requires anonymous=false, and float=false') end
711         if caption then
712           if options['sidecaption'] then
713             spit("\\widefigsidecaption{#{sidepos}}{#{name}}{%\n#{caption}}{labeled}{#{floatpos}}{float}{#{suffix}}{#{dir}}\n")
714           else
715             spit("\\widefig[#{floatpos}]{#{name}}{%\n#{caption}}{#{suffix}}{labeled}{#{dir}}\n")
716           end
717         else
718           die(name,"no caption, but not anonymous")
719         end
720       else # not floating
721         if options['narrowfigwidecaption'] then
722           spit("\\narrowfigwidecaptionnofloat{#{name}}{%\n#{caption}}{#{dir}}\n")
723         else
724           die(name,"The only wide figure that's implemented the option of not floating is narrowfigwidecaption. See comments in lmcommon.sty for explanation.")
725         end
726       end # not floating
727     end # not anonymous
728   end # if wide
729   #----------------------- fullpage ----------------------
730   if width=='fullpage' and options['text']==nil then
731     if options['anonymous'] then
732       if caption then
733         die(name,"the combination of options fullpage+anonymous+caption is not currently supported")
734       else
735         spit("\\fullpagewidthfignocaption[#{dir}]{#{name}}\n")
736       end
737     else # not anonymous
738       if caption then
739         spit("\\fullpagewidthfig[#{dir}]{#{name}}{%\n#{caption}}\n")
740       else
741         die(name,"no caption, but not anonymous")
742       end
743     end
744   end
745   #============================================================================
746   if $fig_handled then
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}}")
752     end
753   else
754     die(name,"not handled")
755   end
758 def spit(tex)
759   print tex
760   $fig_handled = true
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"
766   exit(-1)
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)
777   x = ''
778   File.open(file,'r') { |f|
779     x = f.gets(nil) # nil means read whole file
780   }
781   return x
784 #--------------------------------------------------
785 # code for numbering style used in Fundamentals of Calculus
786 #--------------------------------------------------
788 def hw_block_style
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
801   if s.length==1 then
802     i = "abcdefghijkmnpqrstuvwxyz".index(s)
803     if i.nil? then fatal_error("illegal character #{s} in base24_to_integer") end
804     return i
805   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()
810 if false then
811   [[0,'a'],[1,'b'],[23,'z'],[24,'ba']].each { |x|
812     i = x[0]
813     s = x[1]
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"
816       exit(-1)
817     end
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"
820       exit(-1)
821     end
822   }
825 def hw_freeze
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
830 def hw_end_freeze
831   $hw_freeze = $hw_freeze-1
832   if $hw_freeze<0 then fatal_error("hw_end_freeze invoked, and $hw_freeze already 0? ") end
835 def get_hw_block
836   return integer_to_base24($hw_block)
839 def hw_label
840   label = $hw_number.to_s
841   if hw_block_style() then label = get_hw_block+$hw_number_in_block.to_s end 
842   return label
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'
849 def hw_block(*arg)
850   x = arg[0]
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
868   end_hw
871 def begin_hw(name,difficulty=1,options={})
872   if difficulty==nil then difficulty=1 end
873   if calc() then options['calc']=false end
874   calc = ''
875   if options['calc'] then calc='1' end
876   $hw_number += 1
877   $hw_number_in_block += 1
878   $hw[$hw_number] = name
879   $hw_has_solution[$hw_number] = false
880   label = hw_label()
881   $store_hw_label[$hw_number] = label
882   print "\\begin{homeworkforcelabel}{#{name}}{#{difficulty}}{#{calc}}{#{label}}"
885 def hw_solution()
886   $hw_has_solution[$hw_number] = true # for problems.csv
887   print "\\hwsoln"
888   write_to_answer_data('answer')
891 def hw_hint(label)
892   print "\\hwhint{hwhint:#{label}}"
893   write_to_answer_data('hint')
896 def hw_answer()
897   print "\\hwans{hwans:#{$hw[$hw_number]}}"
898   write_to_answer_data('bare_answer')
901 $answer_data_file = 'answers.csv'
902 $answer_data = []
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"
914   }
917 def read_answer_data()
918   $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])
924     }
925   }
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"
943   read_answer_data()
944   print "\\hwanssection{#{header}}\n\n"
945   last_ch = -1
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
949       name = a[1]
950       if ch==a[0] && type==a[2] then
951         if last_ch!=ch 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+'}\\\\*'
954         end
955         last_ch = ch
956         long = answer_short_label_to_long(name,type)
957         if long==nil then
958           save_complaint("No answer text available for problem #{name}, type #{type}")
959         else
960           print answer_header(long,type)+$answer_text[type][long]
961         end
962       end
963     }
964   end
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
970   }
971   return nil
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)
982   a = []
983   names.each { |name|
984     a.push("p.~\\pageref{hw:#{name}}, \\#\\ref{hw:#{name}}")
985   }
986   print a.join("; ")
989 def hw_ref(name)
990   print "\\ref{hw:#{name}}"
991   $hw_names_referred_to.push(name)
994 def end_hw()
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|
1029       f.print text+"\n\n"
1030     }
1031   end
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" 
1036   end
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"
1039     text = ''
1040   end
1041   if !gave_text then
1042     File.open(file,'r') { |f|
1043       text = f.gets(nil) # nil means slurp whole file
1044     }
1045     text.gsub!(/\n+\Z/) {"\n\n"} # exactly two newlines at the end
1046   end
1047   return text
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}}"
1061 def end_ex
1062   print "\\end{handson}"
1065 # Examples:
1066 #   end_sec()
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='')
1071   debug = false
1072   $count_section_commands += 1
1073   $section_level -= 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}')") 
1080   end
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={})
1089   debug = false
1090   $count_section_commands += 1
1091   if debug then $stderr.print ('  '*$section_level)+"begin_sec(\"#{title}\",#{pagebreak},\"#{label}\")\n" end
1092   $section_level += 1
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
1097     e=''
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"
1101     $section_level = 1
1102   end
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
1110   macro = ''
1111   label_level = ''
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
1118     label_level = 'sec'
1119   end
1120   if $section_level==2 then
1121     if options['toc']==false then
1122       macro = 'mysubsectionnotoc'
1123     else
1124       if options['optional'] then macro='myoptionalsubsection' else macro = 'mysubsection' end
1125     end
1126     label_level = 'subsec'
1127   end
1128   if $section_level==3 then
1129     macro = 'subsubsection'
1130     label_level = 'subsubsec'
1131   end
1132   if $section_level==4 then
1133     macro = 'myssssection'
1134     label_level = 'subsubsubsec'
1135   end
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)
1139   #$stderr.print t
1140   print t
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+/,'_')}"
1147   t = <<-TEX
1148     \\begin{hwsection}[#{title}]
1149     \\anchor{anchor-#{label}}% navigator_package
1150     TEX
1151   if is_prepress then
1152     t = t + "\\addcontentsline{toc}{section}{#{title}}"
1153   else
1154     t = t + "\\addcontentsline{toc}{section}{\\protect\\link{#{label}}{#{title}}}"
1155   end
1156   print t
1159 def end_hw_sec
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]
1166   label_command = ''
1167   complete_label = ''
1168   anchor_command = ''
1169   if label=='' then
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}"
1173     $label_counter += 1
1174   end
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
1179   end
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.
1187   # - 
1188   # similar code in begin_hw_sec
1189   # -
1190   t = <<-TEX
1191     %\\begingroup
1192     \\let\\oldacl\\addcontentsline
1193     \\renewcommand{\\addcontentsline}[3]{}% temporarily disable \\addcontentsline
1194     #{anchor_command_1}#{cmd}#{label_command}#{anchor_command_2}
1195     %\\endgroup
1196     \\let\\addcontentsline\\oldacl
1197     \\#{toc_macro}{#{name_level}}{#{complete_label}}{#{title}}{\\the#{name_level}}
1198     TEX
1199   return t
1202 def is_prepress
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) 
1211   else
1212     return add_titlecase(title)
1213   end
1216 def add_titlecase(title)
1217   foo = title.clone
1218   # Examples:
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} 
1223   }
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
1227   return foo  
1230 $read_proper_nouns = false
1231 $proper_nouns = {}
1232 def proper_nouns
1233   if !$read_proper_nouns then
1234     json_file = whichever_file_exists(["../scripts/proper_nouns.json","scripts/proper_nouns.json"])
1235     json_data = ''
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
1240   end
1241   return $proper_nouns
1244 def remove_titlecase(title)
1245   foo = title.clone
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.
1252   }
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} 
1265   }
1266   acronyms_and_symbols_uppercase(foo) # e.g., FWHM
1267   #if title != foo then $stderr.print "changing title from #{title} to #{foo}\n" end
1268   return foo
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}
1275   }
1278 def initial_cap(x)
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}
1285 def end_chapter
1286   $section_level -= 1
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"
1289   end
1290   $hw_names_referred_to.each { |name|
1291     $stderr.print "hwref:#{name}\n"
1292   }
1293   File.open("ch#{$ch}_problems.csv",'w') { |f|
1294     # book,ch,num,name
1295     book = ENV['BK']
1296     chnum = $ch.to_i
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
1299       name = $hw[i]
1300       f.print "#{book},#{chnum},#{$store_hw_label[i]},#{name},#{$hw_has_solution[i]?'1':'0'}\n"
1301     }
1302   }
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)
1308   files.each {|f|
1309     if File.exist?(f) then return f end
1310   }
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"
1312   return nil
1315 def code_listing(filename,code)
1316   print code  
1317   $n_code_listing = $n_code_listing+1
1318   File.open("code_listing_ch#{$ch}_#{$n_code_listing}_#{filename}",'w') { |f|
1319     f.print code
1320   }
1323 def chapter(number,title,label,caption='',options={})
1324   default_options = {
1325     'opener'=>'',
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
1334   }
1335   $section_level += 1
1336   $ch = number
1337   $label_counter = 0
1338   default_options.each { 
1339     |option,default|
1340     if options[option]==nil then
1341       options[option]=default
1342     end
1343   }
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']
1347   if opener!='' then
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.
1351       s = shared_figs()
1352       dir = s[0]
1353       unless (File.exist?("#{dir}/#{opener}.pdf") or File.exist?("#{dir}/#{opener}.jpg") or File.exist?("#{dir}/#{opener}.png")) then
1354         dir = s[1]
1355       end
1356       options['opener']="../../#{dir}/#{opener}"
1357     end
1358     if options['anonymous']=='default' then
1359       options['anonymous']=(caption=='')
1360     end
1361   end
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)
1370   end
1371   print "\\mychapter{#{title}}\n"
1374 def chapter_print(number,title,label,caption,options)
1375   opener = options['opener']
1376   has_opener = (opener!='')
1377   result = nil
1378   bare_label = label.clone.gsub!(/ch:/,'')
1379   #$stderr.print "in chapter_print, bare_label=#{bare_label}\n"
1380   append = ''
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
1384     if is_prepress
1385       f.print "\\brieftocentry{#{label}}{#{title}} \\\\\n" 
1386     else
1387       f.print "\\brieftocentrywithlink{#{label}}{#{title}} \\\\\n" 
1388     end
1389   }
1390   if !has_opener then
1391     result = "\\mychapter{#{options['short_title']}}{#{options['very_short_title']}}{#{title}}#{append}"
1392   else
1393     opener=~/([^\/]+)$/     # opener could be, e.g., ../../../9share/optics/figs/crepuscular-rays
1394     opener_label = $1
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
1398       if caption!='' then
1399         if !options['sidecaption'] then
1400           if options['special_width']==nil then
1401             result = "\\mychapterwithopener{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1402           else
1403             result = "\\specialchapterwithopener{#{options['special_width']}}{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1404           end
1405         else
1406           if options['anonymous'] then
1407             result = "\\mychapterwithopenersidecaptionanon{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1408           else
1409             result = "\\mychapterwithopenersidecaption{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1410           end
1411         end
1412       else
1413         result = "\\mychapterwithopenernocaption{#{opener}}{#{title}}#{ol}#{append}"
1414       end
1415     else
1416       if options['anonymous'] then
1417         if caption!='' then
1418           result = "\\mychapterwithfullpagewidthopener{#{opener}}{#{caption}}{#{title}}#{ol}#{append}"
1419         else
1420           result = "\\mychapterwithfullpagewidthopenernocaption{#{opener}}{#{title}}#{ol}#{append}"
1421         end
1422       else
1423         $stderr.print "********************************* ch #{ch}full page width chapter openers are only supported as anonymous figures ************************************\n"
1424         exit(-1)
1425       end
1426     end
1427   end
1428   if result=='' then
1429     $stderr.print "**************************************** Error, ch #{$ch}, processing chapter header. ****************************************\n"
1430     exit(-1)
1431   end
1432   print sectioning_command_with_href(result,0,bare_label,'ch',title)
1433   #print "#{result}\\label{#{label}}\n"
1436 $photo_credits = []
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
1448   l = 'fig:'+label
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|
1454     label = c[0]
1455     description = c[1]
1456     credit = c[2]
1457     type = c[3]
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"
1462     else
1463       print "\\docred{Contents}{#{description}}{#{credit}}\n"
1464     end
1465   }
1466   #$ref.keys.each { |k|    print k,$ref[k]  }