Rakefile 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. task default: %w(develop)
  2. begin
  3. require 'colorize'
  4. String.disable_colorization = false
  5. rescue LoadError
  6. end
  7. require 'set'
  8. STORY_DIR=File.join(ENV['USERPROFILE'], 'Documents\\Twine\\Stories')
  9. STORY_TITLE = 'Volleyball'
  10. STORY_FORMAT = 'SugarCube'
  11. STORY_FORMAT_PATH = "story_formats/sugarcube-2.28.2-for-twine-2.1-local/sugarcube-2"
  12. STORY_FORMAT_VERSION = '2.28.2'
  13. STORY_RELEASE_TITLE = 'Volleyball Chapter 1 v0.5'
  14. FILES = {
  15. '01_intro' => '01 Intro',
  16. '02_transform' => '02 Transform',
  17. '03_custom_girl' => '03 Custom Girl',
  18. '04_money_game' => '04 Money Games',
  19. '05_beaches' => '05 Beaches',
  20. '06_public_beach' => '06 Public Beach',
  21. }
  22. def files
  23. FILES.map do |short_name, long_name|
  24. OpenStruct.new({
  25. short_name: short_name,
  26. long_name: long_name,
  27. source_file: "story/#{short_name}.tw2",
  28. dest_file: File.join(STORY_DIR, "#{STORY_TITLE} - #{long_name}.html"),
  29. wrapper_file: "#{short_name}_wrapper.tw2",
  30. })
  31. end
  32. end
  33. task develop: %w(bundle_install storyjs story.tw2 story_title.tw2) do
  34. sh "bundle exec twee2 build '--format=#{STORY_FORMAT_PATH}' main.tw2 develop.html"
  35. end
  36. task watch: %w(bundle_install storyjs story.tw2 story_title.tw2) do
  37. sh 'start watch.html' # FIXME non-windows
  38. sh "bundle exec twee2 watch '--format=#{STORY_FORMAT_PATH}' main.tw2 watch.html"
  39. end
  40. task release: %w(bundle_install storyjs story.tw2 story_title.tw2) do
  41. sh "bundle exec twee2 build '--format=#{STORY_FORMAT_PATH}' main.tw2 release.html"
  42. end
  43. task :storyjs do
  44. File.write(
  45. 'storyjs.tw2',
  46. File.read('storyjs.tw2')\
  47. .sub(/\$STORY_FORMAT_VERSION = '[^']*'/, "$STORY_FORMAT_VERSION = '#{STORY_FORMAT_VERSION}'")
  48. )
  49. end
  50. file 'story.tw2' do
  51. File.open('story.tw2', 'w') do |f|
  52. f.puts('::StoryIncludes')
  53. files.each do |file|
  54. f.puts(file.source_file)
  55. end
  56. end
  57. end
  58. file 'story_title.tw2' do
  59. File.open('story_title.tw2', 'w') do |f|
  60. f.puts('::StoryTitle')
  61. f.puts("#{STORY_RELEASE_TITLE}")
  62. end
  63. end
  64. desc 'export sub files to Twine 2'
  65. task export: %w(bundle_install storyjs story.tw2) do
  66. files.each do |file|
  67. IO.write file.wrapper_file, <<~EOF
  68. ::StoryTitle
  69. Volleyball - #{file.long_name}
  70. ::Twee2Settings [twee2]
  71. @story_start_name = 'Start #{file.long_name}'
  72. Twee2::build_config.story_format = '#{STORY_FORMAT}'
  73. Twee2::build_config.story_format_version = '#{STORY_FORMAT_VERSION}'
  74. Twee2::build_config.story_ifid = ''
  75. ::StoryIncludes
  76. images.tw2
  77. stylesheet.tw2
  78. storyjs.tw2
  79. #{file.source_file}
  80. EOF
  81. sh(
  82. 'bundle', 'exec', 'twee2', 'export',
  83. "--format=#{STORY_FORMAT}", "--format-version=#{STORY_FORMAT_VERSION}",
  84. file.wrapper_file, file.dest_file,
  85. )
  86. end
  87. sh(
  88. 'bundle', 'exec', 'twee2', 'export',
  89. "--format=#{STORY_FORMAT}", "--format-version=#{STORY_FORMAT_VERSION}",
  90. 'main.tw2', STORY_DIR,
  91. )
  92. end
  93. desc 'import sub files from Twine 2'
  94. task import: %w(bundle_install) do
  95. files.each do |file|
  96. sh(
  97. 'bundle', 'exec', 'twee2', 'decompile',
  98. file.dest_file, file.source_file,
  99. '--exclude=StoryJS,StoryCSS,StoryTitle',
  100. '--exclude-from=../stylesheet.tw2,../storyjs.tw2,../stylesheet.tw2,../images.tw2',
  101. )
  102. end
  103. end
  104. task import_main: %w(bundle_install) do
  105. file_set = Set.new(files)
  106. base_excludes=%w[../stylesheet.tw2 ../storyjs.tw2 ../stylesheet.tw2 ../images.tw2]
  107. files.each do |file|
  108. other_files = (file_set - Set.new([file])).map { |f| File.basename(f.source_file) }
  109. excludes = other_files + base_excludes
  110. sh(
  111. 'bundle', 'exec', 'twee2', 'decompile',
  112. File.join(STORY_DIR, STORY_RELEASE_TITLE + '.html'),
  113. './' + file.source_file,
  114. '--exclude=StoryJS,StoryCSS,StoryTitle',
  115. "--exclude-from=#{excludes.join(',')}",
  116. )
  117. end
  118. end
  119. task :bundle_install do
  120. sh 'bundle check || bundle install'
  121. end
  122. task :clean do
  123. # TODO
  124. end
  125. task :autofix do
  126. #check links
  127. files.each do |file|
  128. twee_source = File.read(file.source_file)
  129. twee_source = twee_source.lines.each_with_index.map do |line, lineno|
  130. line.chomp!
  131. line.gsub(%r{\[\[(?<link>[^\]]*?)\](?<link_mods>\[[^\]]*\])?\]}) do |m|
  132. link = $~[:link]
  133. link_mods = $~[:link_mods]
  134. case link.scan('->').length
  135. when 0
  136. # this style of link doesn't need to be quoted, but it also shouldn't contain special chars
  137. "[[#{link}]#{link_mods}]"
  138. when 1
  139. text, dest = link.split('->')
  140. if text.start_with?("'")
  141. text = text.end_with?("'") ? text[1..-2] : text[1..-1]
  142. text.gsub!(/(?<!\\)'/) { "\\'" }
  143. text.gsub!(/(?<!\\)\\(?!')/) { "\\\\" }
  144. text = "'#{text}'"
  145. elsif link.start_with?('"')
  146. text = text.end_with?('"') ? text[1..-2] : text[1..-1]
  147. text.gsub!(/(?<!\\)"/) { "\\\"" }
  148. text.gsub!(/(?<!\\)\\(?!")/) { "\\\\" }
  149. text = "\"#{text}\""
  150. elsif link =~ /\A[\[\]'"\\]*\z/
  151. text.gsub!(/(?<!\\)'/) { "\\'" }
  152. text.gsub!(/(?<!\\)"/) { "\\\"" }
  153. text.gsub!(/(?<!\\)\\(?!['"])/) { "\\\\" }
  154. text = "'" + (text.gsub("'") { "\\'" }) + "'"
  155. end
  156. "[[#{text}->#{dest}]#{link_mods}]"
  157. else
  158. # TODO this doesn't really happen very much in practice, so don't worry about an autofix, just passthru
  159. warn('multiple -> in link')
  160. "[[#{link}]#{link_mods}]"
  161. end
  162. end
  163. =begin
  164. .map do |link|
  165. case link.scan('->').length
  166. when 0
  167. #link contains no ->, probably not a problem
  168. text = dest = link
  169. passages[title][:outbound_links] << \
  170. { passage: dest, source_line: "#{file.source_file}:#{lineno}" }
  171. when 1
  172. # no problem
  173. text, dest = link.split('->')
  174. passages[title][:outbound_links] << \
  175. { passage: dest, source_line: "#{file.source_file}:#{lineno}" }
  176. else
  177. puts "#{file.source_file}:#{lineno} link contains multiple -> definitely a problem".colorize(:red)
  178. next
  179. end
  180. if text.start_with?('\'')
  181. if !text.end_with?('\'')
  182. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) does not end with single quote despite starting with one".colorize(:red)
  183. next
  184. end
  185. text = text[1..-2]
  186. text
  187. .to_enum(:scan, '\'')
  188. .map {Regexp.last_match.offset(0)[0]}
  189. .each do |offset|
  190. if text[offset - 1] != '\\'
  191. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains unquoted single quote in body".colorize(:red)
  192. end
  193. end
  194. elsif text.start_with?('"')
  195. if !text.end_with?('"')
  196. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) does not end with double quote despite starting with one".colorize(:red)
  197. next
  198. end
  199. text = text[1..-2]
  200. text
  201. .to_enum(:scan, '"')
  202. .map {Regexp.last_match.offset(0)[0]}
  203. .each do |offset|
  204. if text[offset - 1] != '\\'
  205. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains unquoted double quote in body".colorize(:red)
  206. end
  207. end
  208. elsif text =~ /\A[\[\]'"\\]*\z/
  209. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains special characters but is not quoted with single or double quotes".colorize(:red)
  210. end
  211. end
  212. =end
  213. end.join("\n") + "\n"
  214. File.write(file.source_file, twee_source)
  215. end
  216. end
  217. task :lint do
  218. titles = Set.new
  219. passages = {}
  220. title = nil
  221. files.each do |file|
  222. twee_source = IO.read(file.source_file)
  223. twee_source.lines.each_with_index do |line, lineno|
  224. lineno += 1
  225. # check for duplicate
  226. if line.start_with?('::') && !line.start_with?('::StoryIncludes')
  227. title = line.match(/^:: *([^\[]*?) *(\[(.*?)\])? *(<(.*?)>)? *$/)[1]
  228. #puts title
  229. if title != 'Twee2Settings' && titles.include?(title)
  230. puts "#{file.source_file}:#{lineno} duplicate title #{title}".red
  231. end
  232. titles.add(title)
  233. passages[title] = {outbound_links: [], inbound_links: [], source_line: "#{file.source_file}:#{lineno}"}
  234. end
  235. # check links
  236. line
  237. .scan(%r{\[\[(.+?)\](\[(.+?)\])?\]})
  238. .map { |x| x[0] }
  239. .each do |link|
  240. case link.scan('->').length
  241. when 0
  242. #link contains no ->, probably not a problem
  243. text = dest = link
  244. passages[title][:outbound_links] << \
  245. { passage: dest, source_line: "#{file.source_file}:#{lineno}" }
  246. when 1
  247. # no problem
  248. text, dest = link.split('->')
  249. passages[title][:outbound_links] << \
  250. { passage: dest, source_line: "#{file.source_file}:#{lineno}" }
  251. else
  252. puts "#{file.source_file}:#{lineno} link contains multiple -> definitely a problem".colorize(:red)
  253. next
  254. end
  255. if text.start_with?('\'')
  256. if !text.end_with?('\'')
  257. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) does not end with single quote despite starting with one".colorize(:red)
  258. next
  259. end
  260. text = text[1..-2]
  261. text
  262. .to_enum(:scan, '\'')
  263. .map {Regexp.last_match.offset(0)[0]}
  264. .each do |offset|
  265. if text[offset - 1] != '\\'
  266. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains unquoted single quote in body".colorize(:red)
  267. end
  268. end
  269. elsif text.start_with?('"')
  270. if !text.end_with?('"')
  271. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) does not end with double quote despite starting with one".colorize(:red)
  272. next
  273. end
  274. text = text[1..-2]
  275. text
  276. .to_enum(:scan, '"')
  277. .map {Regexp.last_match.offset(0)[0]}
  278. .each do |offset|
  279. if text[offset - 1] != '\\'
  280. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains unquoted double quote in body".colorize(:red)
  281. end
  282. end
  283. elsif text =~ /\A[\[\]'"\\]*\z/
  284. puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains special characters but is not quoted with single or double quotes".colorize(:red)
  285. end
  286. end
  287. # check for default text
  288. if line.strip == 'Double-click this passage to edit it.'
  289. #puts "#{file.source_file}:#{lineno} has the default text \"Double-click this passage to edit it.\""
  290. end
  291. # check for TODO/FIXME
  292. if line.include?('TODO') || line.include?('FIXME')
  293. #puts "#{file.source_file}:#{lineno} has TODO/FIXME text: #{line}"
  294. end
  295. # check for <<include>> as string
  296. if line.include?('<<include') && !line.include?('[[')
  297. puts "#{file.source_file}:#{lineno} has an <<include>>, but no link"
  298. end
  299. end
  300. end
  301. passages.each do |title, passage|
  302. passage[:outbound_links].each do |link|
  303. if passages[link[:passage]]
  304. passages[link[:passage]][:inbound_links] << {passage: title, source_file: passage[:source_line]}
  305. else
  306. puts "#{link[:source_line]} (title=#{title}) links to non-existent passage #{link[:passage]}".red
  307. end
  308. end
  309. end
  310. passages.each do |title, passage|
  311. if passage[:inbound_links].empty? && title != 'Twee2Settings'
  312. puts "#{passage[:source_line]} (title=#{title}) has no inbound links"
  313. end
  314. end
  315. end