diff --git a/.gitignore b/.gitignore
index e5337dc..23a4b94 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,7 @@
/.yardoc
/_yardoc/
/coverage/
-/doc/
+/gen/
/pkg/
/spec/reports/
/tmp/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..a722f44
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## v1.0.0
+
+- Initial release
diff --git a/Gemfile b/Gemfile
index d7eae8a..7e5afeb 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,3 +2,6 @@ source "https://rubygems.org"
gem "rake"
gem "rspec"
+gem "rdoc"
+gem "redcarpet"
+gem "syntax"
diff --git a/Gemfile.lock b/Gemfile.lock
index 3b312b7..32b1dd7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,7 +2,12 @@ GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.5.0)
+ psych (5.0.1)
+ stringio
rake (13.0.6)
+ rdoc (6.5.0)
+ psych (>= 4.0.0)
+ redcarpet (3.5.1)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
@@ -16,13 +21,18 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-support (3.11.0)
+ stringio (3.0.4)
+ syntax (1.2.2)
PLATFORMS
ruby
DEPENDENCIES
rake
+ rdoc
+ redcarpet
rspec
+ syntax
BUNDLED WITH
- 2.4.0.dev
+ 2.3.7
diff --git a/Rakefile b/Rakefile
index 3a3c905..6cb1447 100644
--- a/Rakefile
+++ b/Rakefile
@@ -7,3 +7,8 @@ RSpec::Core::RakeTask.new(:spec, :example_pattern) do |task, args|
end
task :default => :spec
+
+desc "Build user guide"
+task :user_guide do
+ system("ruby", "-Ilib", "rb/gen_user_guide.rb")
+end
diff --git a/doc/user_guide.md b/doc/user_guide.md
new file mode 100644
index 0000000..42585c7
--- /dev/null
+++ b/doc/user_guide.md
@@ -0,0 +1,17 @@
+#> Overview
+
+Propane is an LR Parser Generator (LPG) which:
+
+ * accepts LR(0), SLR, and LALR grammars
+ * generates a built-in lexer to tokenize input
+ * supports UTF-8 lexer inputs
+ * generates a table-driven parser to parse input in linear time
+ * is MIT-licensed
+ * is distributable as a standalone Ruby script
+
+${remove}
+WARNING: This user guide is meant to be preprocessed and rendered by a custom
+script.
+The markdown source file is not intended to be viewed directly and will not
+include all intended content.
+${/remove}
diff --git a/rb/assets/user_guide.html.erb b/rb/assets/user_guide.html.erb
new file mode 100644
index 0000000..0faec3b
--- /dev/null
+++ b/rb/assets/user_guide.html.erb
@@ -0,0 +1,68 @@
+
+
+ Propane User Guide<%= subpage_title %> - Version <%= Propane::VERSION %>
+
+
+
+ <%= content %>
+
+
diff --git a/rb/gen_user_guide.rb b/rb/gen_user_guide.rb
new file mode 100644
index 0000000..12c293b
--- /dev/null
+++ b/rb/gen_user_guide.rb
@@ -0,0 +1,227 @@
+#!/usr/bin/env ruby
+
+require "erb"
+require "fileutils"
+require "redcarpet"
+require "syntax"
+require "syntax/convertors/html"
+require "propane/version"
+
+def load_file(file_name)
+ contents = File.read(file_name)
+ contents.gsub(/\$\{include (.*?)\}/) do |match|
+ include_file_name = $1
+ File.read(include_file_name)
+ end
+end
+
+class Generator
+ class Section
+ attr_reader :number
+ attr_reader :title
+ attr_reader :page
+ attr_reader :contents
+ attr_reader :anchor
+ def initialize(number, title, page, anchor)
+ @number = number
+ @title = title.gsub(/[`]/, "")
+ @page = page
+ @anchor = anchor
+ @contents = ""
+ end
+ def append(contents)
+ @contents += contents
+ end
+ end
+
+ class Page
+ attr_reader :name
+ attr_reader :title
+ attr_accessor :contents
+ def initialize(name, title, contents = "")
+ @name = name
+ @title = title
+ @contents = contents
+ end
+ end
+
+ def initialize(input, output_file, multi_page)
+ current_page =
+ if multi_page
+ nil
+ else
+ File.basename(output_file).sub(/\.html$/, "")
+ end
+ @sections = []
+ @current_section_number = [0]
+ @lines = input.lines
+ while @lines.size > 0
+ line = @lines.slice!(0)
+ if line =~ /^```(.*)$/
+ @sections.last.append(gather_code_section($1))
+ elsif line.chomp == "${remove}"
+ remove_section
+ elsif line =~ /^(#+)(>)?\s*(.*)$/
+ level_text, new_page_text, title_text = $1, $2, $3
+ level = $1.size
+ new_page = !new_page_text.nil?
+ section_number = get_next_section_number(level)
+ anchor = make_anchor(section_number, title_text)
+ if multi_page and (new_page or current_page.nil?)
+ current_page = anchor
+ end
+ @sections << Section.new(section_number, title_text, current_page, anchor)
+ @sections.last.append("#{level_text} #{section_number} #{title_text}")
+ elsif @sections.size > 0
+ @sections.last.append(line)
+ end
+ end
+
+ renderer = Redcarpet::Render::HTML.new
+ @markdown_renderer = Redcarpet::Markdown.new(renderer)
+ changelog = @markdown_renderer.render(File.read("CHANGELOG.md"))
+
+ @pages = [Page.new("toc", "Table of Contents", render_toc)]
+ @sections.each do |section|
+ unless @pages.last.name == section.page
+ @pages << Page.new(section.page, "#{section.number} #{section.title}")
+ end
+ @pages.last.contents += render_section(section)
+ end
+
+ @pages.each do |page|
+ page.contents.gsub!("${changelog}", changelog)
+ page.contents.gsub!(%r{\$\{#(.+?)\}}) do |match|
+ section_name = $1
+ href = get_link_to_section(section_name)
+ %[#{section_name}]
+ end
+ end
+
+ template = File.read("rb/assets/user_guide.html.erb")
+ erb = ERB.new(template, trim_mode: "<>")
+
+ if multi_page
+ @pages.each_with_index do |page, page_index|
+ subpage_title = " - #{page.title}"
+ page_nav_bar = render_page_nav_bar(page_index)
+ content = page_nav_bar + separator + page.contents + separator + page_nav_bar
+ html_result = erb.result(binding.clone)
+ File.open(File.join(output_file, "#{page.name}.html"), "w") do |fh|
+ fh.write(html_result)
+ end
+ end
+ else
+ subpage_title = ""
+ content = @pages.reduce("") do |result, page|
+ result + page.contents
+ end
+ html_result = erb.result(binding.clone)
+ File.open(output_file, "w") do |fh|
+ fh.write(html_result)
+ end
+ end
+ end
+
+ def separator
+ %[]
+ end
+
+ def render_page_nav_bar(page_index)
+ page_nav_prev =
+ if page_index > 1
+ %[« Prev
#{@pages[page_index - 1].title}]
+ else
+ ""
+ end
+ page_nav_toc =
+ if page_index > 0
+ %[Table of Contents]
+ else
+ ""
+ end
+ page_nav_next =
+ if page_index < @pages.size - 1
+ %[Next »
#{@pages[page_index + 1].title}]
+ else
+ ""
+ end
+ %[] + \
+ %[#{page_nav_prev} | ] + \
+ %[#{page_nav_toc} | ] + \
+ %[#{page_nav_next} | ] + \
+ %[
]
+ end
+
+ def render_toc
+ toc_content = %[Table of Contents
\n]
+ @sections.each do |section|
+ indent = section.number.split(".").size - 1
+ toc_content += %[]
+ toc_content += %[#{section.number} #{section.title}
\n]
+ toc_content += %[]
+ end
+ toc_content
+ end
+
+ def render_section(section)
+ %[] + \
+ @markdown_renderer.render(section.contents)
+ end
+
+ def make_anchor(section_number, section_title)
+ "s" + ("#{section_number} #{section_title}").gsub(/[^a-zA-Z0-9]/, "_")
+ end
+
+ def get_link_to_section(section_name)
+ section = @sections.find do |section|
+ section.title == section_name
+ end
+ raise "Could not find section #{section_name}" unless section
+ "#{section.page}.html##{section.anchor}"
+ end
+
+ def gather_code_section(syntax)
+ code = ""
+ loop do
+ line = @lines.slice!(0)
+ if line =~ /^```/
+ break
+ end
+ code += line
+ end
+ if syntax != ""
+ convertor = Syntax::Convertors::HTML.for_syntax(syntax)
+ %[\n#{convertor.convert(code)}\n
\n]
+ else
+ %[\n]
+ end
+ end
+
+ def remove_section
+ loop do
+ line = @lines.slice!(0)
+ if line.chomp == "${/remove}"
+ break
+ end
+ end
+ end
+
+ def get_next_section_number(level)
+ if @current_section_number.size == level - 1
+ @current_section_number << 1
+ elsif @current_section_number.size >= level
+ @current_section_number[level - 1] += 1
+ @current_section_number.slice!(level, @current_section_number.size)
+ else
+ raise "Section level change from #{@current_section_number.size} to #{level}"
+ end
+ @current_section_number.join(".")
+ end
+end
+
+input = load_file("doc/user_guide.md")
+FileUtils.rm_rf("gen/user_guide")
+FileUtils.mkdir_p("gen/user_guide")
+Generator.new(input, "gen/user_guide/user_guide.html", false)
+Generator.new(input, "gen/user_guide", true)