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 + %[] + \ + %[] + \ + %[] + \ + %[] + \ + %[] + 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
#{code}
\n
\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)