task default: %w(develop) begin require 'colorize' String.disable_colorization = false rescue LoadError end require 'set' STORY_DIR=File.join(ENV['USERPROFILE'], 'Documents\\Twine\\Stories') STORY_TITLE = 'Volleyball' STORY_FORMAT = 'SugarCube' STORY_FORMAT_PATH = "story_formats/sugarcube-2.28.2-for-twine-2.1-local/sugarcube-2" STORY_FORMAT_VERSION = '2.28.2' STORY_RELEASE_TITLE = 'Volleyball Chapter 1 v0.5' FILES = { '01_intro' => '01 Intro', '02_transform' => '02 Transform', '03_custom_girl' => '03 Custom Girl', '04_money_game' => '04 Money Games', '05_beaches' => '05 Beaches', '06_public_beach' => '06 Public Beach', } def files FILES.map do |short_name, long_name| OpenStruct.new({ short_name: short_name, long_name: long_name, source_file: "story/#{short_name}.tw2", dest_file: File.join(STORY_DIR, "#{STORY_TITLE} - #{long_name}.html"), wrapper_file: "#{short_name}_wrapper.tw2", }) end end task develop: %w(bundle_install storyjs story.tw2 story_title.tw2) do sh "bundle exec twee2 build '--format=#{STORY_FORMAT_PATH}' main.tw2 develop.html" end task watch: %w(bundle_install storyjs story.tw2 story_title.tw2) do sh 'start watch.html' # FIXME non-windows sh "bundle exec twee2 watch '--format=#{STORY_FORMAT_PATH}' main.tw2 watch.html" end task release: %w(bundle_install storyjs story.tw2 story_title.tw2) do sh "bundle exec twee2 build '--format=#{STORY_FORMAT_PATH}' main.tw2 release.html" end task :storyjs do File.write( 'storyjs.tw2', File.read('storyjs.tw2')\ .sub(/\$STORY_FORMAT_VERSION = '[^']*'/, "$STORY_FORMAT_VERSION = '#{STORY_FORMAT_VERSION}'") ) end file 'story.tw2' do File.open('story.tw2', 'w') do |f| f.puts('::StoryIncludes') files.each do |file| f.puts(file.source_file) end end end file 'story_title.tw2' do File.open('story_title.tw2', 'w') do |f| f.puts('::StoryTitle') f.puts("#{STORY_RELEASE_TITLE}") end end desc 'export sub files to Twine 2' task export: %w(bundle_install storyjs story.tw2 story_title.tw2) do files.each do |file| IO.write file.wrapper_file, <<~EOF ::StoryTitle Volleyball - #{file.long_name} ::Twee2Settings [twee2] @story_start_name = 'Start #{file.long_name}' Twee2::build_config.story_format = '#{STORY_FORMAT}' Twee2::build_config.story_format_version = '#{STORY_FORMAT_VERSION}' Twee2::build_config.story_ifid = '' ::StoryIncludes images.tw2 stylesheet.tw2 storyjs.tw2 #{file.source_file} EOF sh( 'bundle', 'exec', 'twee2', 'export', "--format=#{STORY_FORMAT}", "--format-version=#{STORY_FORMAT_VERSION}", file.wrapper_file, file.dest_file, ) end sh( 'bundle', 'exec', 'twee2', 'export', "--format=#{STORY_FORMAT}", "--format-version=#{STORY_FORMAT_VERSION}", 'main.tw2', STORY_DIR, ) end desc 'import sub files from Twine 2' task import: %w(bundle_install) do files.each do |file| sh( 'bundle', 'exec', 'twee2', 'decompile', file.dest_file, file.source_file, '--exclude=StoryJS,StoryCSS,StoryTitle,Twee2Settings', '--exclude-from=../stylesheet.tw2,../storyjs.tw2,../stylesheet.tw2,../images.tw2', ) end end task import_main: %w(bundle_install) do file_set = Set.new(files) base_excludes=%w[../stylesheet.tw2 ../storyjs.tw2 ../stylesheet.tw2 ../images.tw2] files.each do |file| other_files = (file_set - Set.new([file])).map { |f| File.basename(f.source_file) } excludes = other_files + base_excludes sh( 'bundle', 'exec', 'twee2', 'decompile', File.join(STORY_DIR, STORY_RELEASE_TITLE + '.html'), './' + file.source_file, '--exclude=StoryJS,StoryCSS,StoryTitle,Twee2Settings', "--exclude-from=#{excludes.join(',')}", ) end end task :bundle_install do sh 'bundle check || bundle install' end task :clean do # TODO end task :autofix do #check links files.each do |file| twee_source = File.read(file.source_file) twee_source = twee_source.lines.each_with_index.map do |line, lineno| line.chomp! line.gsub(%r{\[\[(?[^\]]*?)\](?\[[^\]]*\])?\]}) do |m| link = $~[:link] link_mods = $~[:link_mods] case link.scan('->').length when 0 # this style of link doesn't need to be quoted, but it also shouldn't contain special chars "[[#{link}]#{link_mods}]" when 1 text, dest = link.split('->') quote_text = true if link.start_with?('"') && text.include?("\\") text = text.end_with?('"') ? text[1..-2] : text[1..-1] elsif text.start_with?("'") text = text.end_with?("'") ? text[1..-2] : text[1..-1] elsif text =~ /\\|"/ quote_text = true else quote_text = false end if quote_text text.gsub!(/(?#{dest}]#{link_mods}]" else # TODO this doesn't really happen very much in practice, so don't worry about an autofix, just passthru warn('multiple -> in link') "[[#{link}]#{link_mods}]" end end end.join("\n") + "\n" File.write(file.source_file, twee_source) end end task :lint do titles = Set.new passages = {} title = nil files.each do |file| twee_source = IO.read(file.source_file) twee_source.lines.each_with_index do |line, lineno| lineno += 1 # check for duplicate if line.start_with?('::') && !line.start_with?('::StoryIncludes') title = line.match(/^:: *([^\[]*?) *(\[(.*?)\])? *(<(.*?)>)? *$/)[1] #puts title if title != 'Twee2Settings' && titles.include?(title) puts "#{file.source_file}:#{lineno} duplicate title #{title}".red end titles.add(title) passages[title] = {outbound_links: [], inbound_links: [], source_line: "#{file.source_file}:#{lineno}"} end # check links line .scan(%r{\[\[(.+?)\](\[(.+?)\])?\]}) .map { |x| x[0] } .each do |link| case link.scan('->').length when 0 #link contains no ->, probably not a problem text = dest = link passages[title][:outbound_links] << \ { passage: dest, source_line: "#{file.source_file}:#{lineno}" } when 1 # no problem text, dest = link.split('->') passages[title][:outbound_links] << \ { passage: dest, source_line: "#{file.source_file}:#{lineno}" } else puts "#{file.source_file}:#{lineno} link contains multiple -> definitely a problem".colorize(:red) next end if text.start_with?('\'') if !text.end_with?('\'') puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) does not end with single quote despite starting with one".colorize(:red) next end text = text[1..-2] text .to_enum(:scan, '\'') .map {Regexp.last_match.offset(0)[0]} .each do |offset| if text[offset - 1] != '\\' puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains unquoted single quote in body".colorize(:red) end end elsif text.start_with?('"') if !text.end_with?('"') puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) does not end with double quote despite starting with one".colorize(:red) next end text = text[1..-2] text .to_enum(:scan, '"') .map {Regexp.last_match.offset(0)[0]} .each do |offset| if text[offset - 1] != '\\' puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains unquoted double quote in body".colorize(:red) end end elsif text =~ /\A[\[\]'"\\]*\z/ puts "#{file.source_file}:#{lineno} link text (#{text.inspect}) contains special characters but is not quoted with single or double quotes".colorize(:red) end end # check for default text if line.strip == 'Double-click this passage to edit it.' puts "#{file.source_file}:#{lineno} has the default text \"Double-click this passage to edit it.\"" end # check for TODO/FIXME if line.include?('TODO') || line.include?('FIXME') puts "#{file.source_file}:#{lineno} has TODO/FIXME text: #{line}" end # check for <> as string if line.include?('<>, but no link" end end end passages.each do |title, passage| passage[:outbound_links].each do |link| if passages[link[:passage]] passages[link[:passage]][:inbound_links] << {passage: title, source_file: passage[:source_line]} else puts "#{link[:source_line]} (title=#{title}) links to non-existent passage #{link[:passage]}".red end end end passages.each do |title, passage| if passage[:inbound_links].empty? && title != 'Twee2Settings' puts "#{passage[:source_line]} (title=#{title}) has no inbound links" end end end