Browse Source

More linting of kaysermake.lua

Urs Liska 2 years ago
parent
commit
fb03fa6d08
2 changed files with 179 additions and 67 deletions
  1. 177
    65
      latex/kaysermake.lua
  2. 2
    2
      make.ly

+ 177
- 65
latex/kaysermake.lua View File

@@ -16,18 +16,16 @@ local md5 = require 'md5'
16 16
 -- kayser object is returned from this file
17 17
 ------------------------------------------------------
18 18
 
19
-local kayser = {}
20
-
21 19
 -- included submodules
22
-local util = {}
23
-local setup = {}
24
-local repo = {}
25
-local meta = {}
26
-local titles = {}
27
-local md = {}
28
-local lily = {}
29 20
 local content = {}
21
+local lily = {}
22
+local meta = {}
23
+local repo = {}
24
+local setup = {}
25
+local util = {}
26
+
27
+-- public interface
28
+local kayser = {}
30 29
 
31 30
 ------------------------------------------------------
32 31
 -- Constants and defaults
@@ -254,6 +252,8 @@ local VOLUME_TEMPLATE = [[
254 252
 
255 253
 --[[
256 254
 ------------------------------------------------------
255
+  CONTENT
256
+
257 257
   Deal with the 'content' of the generated .tex volume.
258 258
   All make_XX functions set a field in the `content` table
259 259
   to be written to the generated document later.
@@ -314,6 +314,11 @@ function content.make_geometry()
314 314
   bottom=%scm}
315 315
 ]],
316 316
   2, 1.5, 1, 1)
317
+  --[[
318
+    content.geometry is first "printed" directly to set the context
319
+    for lyluatex to pass layout to LilyPond.
320
+    Additionally it will later be used in the generated .tex document.
321
+  --]]
317 322
   util.print_latex(content.geometry)
318 323
 end
319 324
 
@@ -327,7 +332,7 @@ end
327 332
 function content.make_original_title()
328 333
   local cnt = ''
329 334
   if OPTIONS.scoretype ~= 'part' then
330
-    cnt = util.read_file(repo.workgroup_path()..'cover.tex')..[[\par\vfill]]
335
+    cnt = util.read_file(repo.workgroup_path()..'cover.tex', '')..[[\par\vfill]]
331 336
   end
332 337
   content.originaltitle = cnt
333 338
 end
@@ -484,8 +489,9 @@ end
484 489
 --]]
485 490
 function meta.get_doc_tex_list()
486 491
   local result = {}
487
-  for i, vol in ipairs(meta.get_volumes()) do
488
-    result[i] = string.format('\\item volumes/%s-%s-%s.pdf',
492
+  local volumes = meta.get_volumes()
493
+  for i, vol in ipairs(volumes) do
494
+    result[i] = string.format('\\item volumes/%s/%s/%s.pdf',
489 495
       OPTIONS.workgroup,
490 496
       OPTIONS.work,
491 497
       vol)
@@ -494,6 +500,12 @@ function meta.get_doc_tex_list()
494 500
 end
495 501
 
496 502
 
503
+--[[
504
+  Retrieve a given property from an instrument definition.
505
+  Takes indexed instrument parts into account.
506
+  Returns the stored property value plus the instrument index
507
+  (0 for unindexed parts).
508
+--]]
497 509
 function meta.get_instrument_property(instrument, property)
498 510
   local basename, index = util.indexed_name(instrument)
499 511
   local inst = META.instruments[basename]
@@ -504,7 +516,11 @@ function meta.get_instrument_property(instrument, property)
504 516
   return inst[property], index
505 517
 end
506 518
 
507
-
519
+--[[
520
+  Retrieve a list of the part display names for the current work.
521
+  Respects the instrument index, but so far only adds an integer
522
+  to an indexed part.
523
+--]]
508 524
 function meta.get_part_names()
509 525
   --NOTE: WIP
510 526
   local result = ''
@@ -521,10 +537,12 @@ end
521 537
 --[[
522 538
   Returns a list/table (of potentially 1) with the volumes
523 539
   to be printed. Multiple entries can only be the result of
524
-  specifying 'parts' as the rendering target.
540
+  specifying 'parts' or 'all' as the 'part' option.
525 541
 ]]
526 542
 function meta.get_volumes()
527
-  return OPTIONS.volumes or { [1] = OPTIONS.part }
543
+  if #OPTIONS.volumes == 0 then return { [1] = OPTIONS.part }
544
+  else return OPTIONS.volumes
545
+  end
528 546
 end
529 547
 
530 548
 
@@ -541,18 +559,19 @@ end
541 559
 function meta.read_file(file_name, function_name)
542 560
   local metadata = {}
543 561
   local content = util.read_file(file_name)
544
-  local expression, _ = util.split_with_block(content, function_name)
545
-  if not expression then error(string.format([[
562
+  local with_expression, _ = util.split_with_block(content, function_name)
563
+  if not with_expression then error(string.format([[
546 564
 No function \%s found in file %s.]], function_name, file_name))
547 565
   end
548
-
549
-  return util.parse_with_block(expression)
566
+  return util.parse_with_block(with_expression)
550 567
 end
551 568
 
552 569
 
570
+--[[
571
+  Read all the instrument definitions from ROOT/config/instruments.ily
572
+  and store them in META.instruments
573
+--]]
553 574
 function meta.read_instrument_defs()
554
-  --[[Read all the instrument definitions from ROOT/config/instruments.ily
555
-  and store them in META.instruments]]
556 575
   local content, key, expression, tail
557 576
   META.instruments = {}
558 577
   content = util.read_file(repo.root()..'config/instruments.ily')
@@ -603,13 +622,6 @@ function meta.read_work_metadata()
603 622
     ['key-mode'] = work_meta['key-mode'],
604 623
     ['movements'] = util.parse_symbol_list(work_meta.movements)
605 624
   }
606
-  OPTIONS.volumes = {}
607
-  if OPTIONS.part == 'parts' then meta.set_inst_parts()
608
-  elseif OPTIONS.part == 'all' then
609
-    table.insert(OPTIONS.volumes, 'score')
610
-    table.insert(OPTIONS.volumes, 'choir')
611
-    meta.set_inst_parts()
612
-  end
613 625
 end
614 626
 
615 627
 
@@ -625,9 +637,11 @@ function meta.read_workgroup_metadata()
625 637
 end
626 638
 
627 639
 
640
+--[[
641
+  Add instrumental parts to OPTIONS.volumes
642
+  i.e. all parts within the current work that are not vocal parts
643
+  (since vocal parts are only printed as part of the 'choir' score)
644
+--]]
628 645
 function meta.set_inst_parts()
629 646
   local i = 1
630 647
   result = {}
@@ -640,25 +654,38 @@ function meta.set_inst_parts()
640 654
   for _, part in ipairs(META.work.parts) do
641 655
     if not vocals[part] then table.insert(OPTIONS.volumes, part) end
642 656
   end
643 657
 end
644 658
 
645 659
 
660
+
646 661
 ------------------------------------------------------
647 662
 -- "repo property" functions
648 663
 -- Root and all paths end with a slash
649 664
 ------------------------------------------------------
650 665
 
666
+--[[
667
+  Check if the path ROOT/volumes/<workgroup>/<work> exists
668
+  and create the directories if necessary.
669
+  Return ROOT/volumes/<workgroup>/<work>/<part>.tex
670
+--]]
671
+function repo.check_volume_path()
672
+  local workgroup_path = repo.root()..'volumes/'..OPTIONS.workgroup..'/'
673
+  if not util.exists(workgroup_path) then
674
+    lfs.mkdir(workgroup_path)
675
+  end
676
+  local work_path = workgroup_path..OPTIONS.work..'/'
677
+  if not util.exists(work_path) then
678
+    lfs.mkdir(work_path)
679
+  end
680
+  return work_path..OPTIONS.part..'.tex'
681
+end
682
+
683
+
651 684
 function repo.edition_metadata_file()
652 685
   return repo._root..'/works/metadata.ily'
653 686
 end
654 687
 
688
+
655 689
 --[[
656 690
   Finds a file in a given directory.
657 691
   If ext is given (without leading dot) then the exact file is searched,
@@ -735,7 +762,13 @@ end
735 762
 -- Set up the whole process
736 763
 ------------------------------------------------------
737 764
 
765
+--[[
766
+  Configure the process.
767
+  - Let command line options override defaults or settings from inside the doc
768
+  - Determine the target work
769
+  - Optionally process further option
770
+    that have to be dealt with after the targe3t
771
+--]]
738 772
 function setup.configure()
739 773
   setup.process_cmd_line_opts()
740 774
   setup.process_target()
@@ -766,7 +799,7 @@ end
766 799
   Can be used to do further configuration based on the actual options.
767 800
 --]]
768 801
 function setup.process_options()
769
-
802
+-- Currently this is a no-op
770 803
 end
771 804
 
772 805
 
@@ -821,14 +854,36 @@ function setup.set_scoretype()
821 854
 end
822 855
 
823 856
 
857
+--[[
858
+  Configure a list of volumes to be created, depending on the requested 'part'.
859
+  Can be
860
+  - a single part (must exist in the work)
861
+  - score
862
+  - choir
863
+  - parts (all instrumental parts for the current work)
864
+  - all (score, choir, and all parts)
865
+--]]
866
+function setup.set_volumes()
867
+  OPTIONS.volumes = {}
868
+  if OPTIONS.part == 'parts' then meta.set_inst_parts()
869
+  elseif OPTIONS.part == 'all' then
870
+    table.insert(OPTIONS.volumes, 'score')
871
+    table.insert(OPTIONS.volumes, 'choir')
872
+    meta.set_inst_parts()
873
+  end
874
+end
875
+
876
+
824 877
 
825 878
 ------------------------------------------------------
826 879
 -- General utility functions
827 880
 ------------------------------------------------------
828 881
 
882
+--[[
883
+  Check if a file or directory exists in this path
884
+  From https://stackoverflow.com/a/40195356/2107724
885
+  A pity this doesn't exist in Lua itself
886
+--]]
829 887
 function util.exists(file)
830 888
    local ok, err, code = os.rename(file, file)
831 889
    if not ok then
@@ -857,10 +912,12 @@ function util.indexed_name(name)
857 912
 end
858 913
 
859 914
 
860
-function util.parse_scheme_pair(input)
861
-  --[[Parse a string that represents a (LilyPond) Scheme pair #'(key . value)
915
+--[[
916
+  Parse a string that represents a (LilyPond) Scheme pair #'(key . value)
862 917
   If either of the values is a string (surrounded by double quotes) these are
863
-  stripped off and the bare content is returned.]]
918
+  stripped off and the bare content is returned.
919
+--]]
920
+function util.parse_scheme_pair(input)
864 921
   input = input:match('#\'%((.*)%)')
865 922
   if not input then return { ['car'] = "", ['cdr'] = "" } end
866 923
   local car, cdr = input:match('(.*) %. (.*)')
@@ -868,9 +925,15 @@ function util.parse_scheme_pair(input)
868 925
   return util.unstringify(car), util.unstringify(cdr)
869 926
 end
870 927
 
871
-
928
+--[[
929
+  Parse a Scheme symbol list and return a table.
930
+  Input may be
931
+  - a complete Scheme list #'(elt1 elt2 elt3 ...)
932
+  - a list in dot notation elt1.elt2.elt3
933
+  - a single string/symbol
934
+--]]
872 935
 function util.parse_symbol_list(input)
873
-  scheme_list = input:match("#'%((.*)%)")
936
+  local scheme_list = input:match("#'%((.*)%)")
874 937
   if scheme_list then return scheme_list:explode(' ')
875 938
   else
876 939
     return input:explode('.')
@@ -878,20 +941,22 @@ function util.parse_symbol_list(input)
878 941
 end
879 942
 
880 943
 
881
-function util.parse_with_block(input)
882
-  --[[Parses the contents of a \with {} block,
883
-  returning a table with all key=value assignments.
944
+--[[
945
+  Parse a LilyPond \with {} block
946
+  and return a table with all key=value assignments.
884 947
   Assignments spanning multiple lines are concatenated to a single string,
885 948
   replacing the whitespace with a single space. This allows the layout of
886 949
   e.g. Scheme pairs and lists over multiple lines.
887 950
   Line comments are ignored.
888 951
   NOTE: The "values" are returned as strings, so any "Scheme-y" interpretation
889 952
   has to be done afterwards.
890
-  NOTE: While it is allowed in LilyPond comments *after* the value are not
891
-  supported.
953
+  NOTE: While it is allowed in LilyPond
954
+  comments *after* the value are not supported.
892 955
   NOTE: While this would be highly unusual it is possible in LilyPond language
893 956
   to place multiple assignments on a single line. This is not supported,
894
-  "key =" must be the first thing on a new line.]]
957
+  "key =" must be the first thing on a new line.
958
+--]]
959
+function util.parse_with_block(input)
895 960
   local props = {}
896 961
   local prev_key = nil
897 962
   local prev = ''
@@ -899,6 +964,7 @@ function util.parse_with_block(input)
899 964
   for i, line in ipairs(input) do
900 965
     -- ignore empty lines and comments
901 966
     if (line ~= '') and (not line:match('%s*%%')) then
967
+      -- skip, key, equal, value, remainder
902 968
       s, k, eq, v, r = line:match('(%s*)(%g*)(%s*=%s*)(.*)(%s*)')
903 969
       if not eq then
904 970
         -- line continues a previous option, concat to previous option
@@ -933,14 +999,16 @@ end
933 999
 
934 1000
 
935 1001
 -- Read a file and return its content as a string.
936
-function util.read_file(file_name)
1002
+function util.read_file(file_name, default)
937 1003
   local f = io.open(file_name, 'r')
938 1004
   if f then
939 1005
     local content = f:read('*a')
940 1006
     f:close()
941 1007
     return content
942 1008
   else
943
-    err(string.format([[File %s not found]], file_name))
1009
+    if default then return default else
1010
+      err(string.format([[File %s not found]], file_name))
1011
+    end
944 1012
   end
945 1013
 end
946 1014
 
@@ -1003,14 +1071,16 @@ function util.split_with_block(input, function_name)
1003 1071
 end
1004 1072
 
1005 1073
 
1006
-function util.tex_from_md(filename)
1007
-  --[[Return a string with TeX code converted from Markdown.
1074
+--[[
1075
+  Return a string with TeX code converted from Markdown (by Pandoc).
1008 1076
   The Markdown file should be plain Markdown (without YAML header).
1009 1077
   If the file is not found an empty string is returned.
1010 1078
   The function uses the MD5 hash of the Markdown content to determine
1011 1079
   if this file has already been converted earlier. The Markdown content
1012 1080
   is converted to a .tex file in the cache directory using Pandoc.
1013
-  NOTE: there is no mechanism to purge unused .tex files (yet).]]
1081
+  NOTE: there is no mechanism to purge unused .tex files (yet).
1082
+--]]
1083
+function util.tex_from_md(filename)
1014 1084
   local f, md_content, cachename, tex_content
1015 1085
   f = io.open(filename, 'r')
1016 1086
   if not f then return '' end
@@ -1081,7 +1151,11 @@ function kayser.process_content()
1081 1151
 end
1082 1152
 
1083 1153
 
1154
+--[[
1155
+  Set an option or an engrave option.
1156
+  Only keys present in KNOWN_OPTIONS or ENGRAVE_OPTIONS
1157
+  are allowed.
1158
+--]]
1084 1159
 function kayser.set_option(k, v)
1085 1160
   if KNOWN_OPTIONS[k] then
1086 1161
     OPTIONS[k] = v
@@ -1093,11 +1167,18 @@ function kayser.set_option(k, v)
1093 1167
 end
1094 1168
 
1095 1169
 
1170
+--[[
1171
+  Set the score type for the current score/part.
1172
+  These functions first write something directly to the current document
1173
+  (configuring the document for lyluatex) and keep a copy of the TeX code
1174
+  for later reuse when generating the .tex documents.
1175
+--]]
1096 1176
 function kayser.set_scoretype()
1097 1177
   setup.set_scoretype()
1098 1178
   content.make_geometry()
1099 1179
 end
1100 1180
 
1181
+
1101 1182
 --[[
1102 1183
   Prepare compilation.
1103 1184
   Configure options and target, retrieve metadata for the requested work,
@@ -1106,46 +1187,60 @@ end
1106 1187
 function kayser.setup()
1107 1188
   setup.configure()
1108 1189
   meta.read_metadata()
1190
+  setup.set_volumes()
1109 1191
   util.print_latex(string.format(
1110 1192
     DUMMY_TEMPLATE,
1111 1193
     table.concat(meta.get_doc_tex_list(), '\n')))
1112 1194
 end
1113 1195
 
1114 1196
 
1197
+--[[
1198
+  Create the code for the foreach loop with the parts
1199
+  It is necessary to have that strange separation of concerns
1200
+  because we need to be in Lua to iterate over the parts
1201
+  but have to go back to TeX for each iteration.
1202
+--]]
1115 1203
 function kayser.start_part_loop()
1116 1204
   tex.sprint(string.format(
1117 1205
     [[\foreach \kpart in {%s}]],
1118
-    table.concat(meta.get_volumes(), ','))
1119
-  )
1206
+    table.concat(meta.get_volumes(), ',')))
1120 1207
 end
1121 1208
 
1122
-
1209
+--[[
1210
+  Write a volume to an intermediate .tex file
1211
+  ROOT/volumes/<workgroup>/<work>/<part>.tex
1212
+  and then invoke lualatex (again) to compile that to PDF.
1213
+  The intermediate files don't depend on lyluatex and make
1214
+  use of the generated scores through their hash cachnames.
1215
+--]]
1123 1216
 function kayser.write_volume(cachename)
1217
+  --[[
1218
+    We only have access to the necessary data now, therefore
1219
+    we have to generate the LaTeX code for the score inclusion
1220
+    only now.
1221
+  --]]
1124 1222
   content.make_score(cachename)
1125
-  local doc = util.replace(VOLUME_TEMPLATE, content):gsub(
1126
-    '\n +', '\n')
1127
-  local workgroup_path = repo.root()..'volumes/'..OPTIONS.workgroup..'/'
1128
-  if not util.exists(workgroup_path) then
1129
-    lfs.mkdir(workgroup_path)
1130
-  end
1131
-  local work_path = workgroup_path..OPTIONS.work..'/'
1132
-  if not util.exists(work_path) then
1133
-    lfs.mkdir(work_path)
1134
-  end
1135
-  local full_outname = work_path..OPTIONS.part..'.tex'
1223
+
1224
+  -- Replace the placeholders in the template with the
1225
+  -- material stored in the 'content' table
1226
+  local doc = util.replace(VOLUME_TEMPLATE, content):gsub('\n +', '\n')
1227
+
1228
+  -- Determine the target name and create directories if necessary
1229
+  local outname = repo.check_volume_path()
1230
+
1231
+  -- Generate intermediate .tex file
1136 1232
   print()
1137
-  print("Exporting volume to "..full_outname)
1138
-  f = io.open(full_outname, 'w')
1233
+  print("Exporting volume to "..outname)
1234
+  --TODO: Compare existing file with hash to avoid unnecessary recompilations
1235
+  f = io.open(outname, 'w')
1139 1236
   f:write(doc)
1140 1237
   f:close()
1141 1238
 
1239
+  -- Compile the intermediate fileßs to .pdf
1142 1240
   print("Compiling volume ... ")
1143
-  local tex_cmd = string.format([[latexmk --gg --output-directory=%s --lualatex %s]],
1241
+  local tex_cmd = string.format([[latexmk --gg --interaction=nonstopmode --output-directory=%s --lualatex %s]],
1144 1242
 work_path,
1145
-full_outname)
1146
-print("latexmk command")
1147
-print(tex_cmd)
1243
+outname)
1148 1244
   p = io.popen(tex_cmd)
1149 1245
   result = p:read('*all')
1150 1246
   if not p then err("Error starting lualatex") end

+ 2
- 2
make.ly View File

@@ -10,8 +10,8 @@
10 10
 % Given values must match directory/file names.
11 11
 \engrave
12 12
 \with {
13
-  workgroup = masses
14
-  work = three
13
+  workgroup = cantatas
14
+  work = one
15 15
   movement = all
16 16
   part = score
17 17