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('->')
if text.start_with?("'")
text = text.end_with?("'") ? text[1..-2] : text[1..-1]
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