require 'erubi'
require 'rouge'
require 'redcarpet'
require 'tmpdir'
module RTFDoc
class AttributesComponent
def initialize(raw_attrs, title)
@attributes = YAML.load(raw_attrs)
@title = title
end
template = Erubi::Engine.new(File.read(File.expand_path('../src/attributes.erb', __dir__)))
class_eval <<-RUBY
define_method(:output) { #{template.src} }
RUBY
end
class Renderer < Redcarpet::Render::Base
attr_reader :rouge_formatter, :rouge_lexer
def initialize(*args)
super
@rouge_formatter = Rouge::Formatters::HTML.new
@rouge_lexer = Rouge::Lexers::JSON.new
end
def emphasis(text)
"#{text}"
end
def double_emphasis(text)
"#{text}"
end
def paragraph(text)
"
#{text}
"
end
def header(text, level)
if level == 4
%()
else
"#{text}"
end
end
def table(header, body)
<<-HTML
HTML
ensure
@table_title = nil
end
def table_row(content)
content.empty? ? nil : "#{content}
"
end
def table_cell(content, alignment)
if !alignment
@table_title = content unless content.empty?
return
end
c = case alignment
when 'left' then 'definition'.freeze
when 'right' then 'property'.freeze
end
"#{content} | "
end
def block_html(raw_html)
raw_html
end
def codespan(code)
"#{code}
"
end
def block_code(code, language)
if language == 'attributes' || language == 'parameters'
AttributesComponent.new(code, language).output
elsif language == 'response'
<<-HTML
RESPONSE
#{rouge_formatter.format(rouge_lexer.lex(code.strip))}
HTML
end
end
end
class Template
def initialize(sections)
@content = sections.map(&:output).join
@menu_content = sections.map(&:menu_output).join
end
def output
template = Erubi::Engine.new(File.read(File.expand_path('../src/index.html.erb', __dir__)))
eval(template.src)
end
end
module RenderAsSection
def self.included(other)
other.attr_accessor(:include_show_button)
end
template = Erubi::Engine.new(File.read(File.expand_path('../src/section.erb', __dir__)))
module_eval <<-RUBY
define_method(:output) { #{template.src} }
RUBY
def content_to_html
RTFDoc.markdown_to_html(@content)
end
def example_to_html
@example ? RTFDoc.markdown_to_html(@example) : nil
end
end
module Anchorable
def anchor(content, class_list: nil)
%(#{content}"
end
end
class Section
include RenderAsSection
include Anchorable
attr_reader :name, :method, :path
def initialize(name, raw_content, resource: nil)
@name = name
@resource = resource
metadata = nil
if raw_content.start_with?('---')
idx = raw_content.index('---', 4)
raise 'bad format' unless idx
parse_metadata(YAML.load(raw_content.slice!(0, idx + 3)))
end
raise 'missing metadata' if resource && !@path && !@method
@content, @example = raw_content.split('$$$')
end
def id
@id ||= name
end
def anchor_id
@resource ? "#{@resource}-#{id}" : id
end
def resource_name
@resource
end
def menu_output
"#{anchor(menu_title)}"
end
def signature
sig = <<-HTML.strip!
HTML
anchor(sig)
end
private
def menu_title
@menu_title || name.capitalize
end
def parse_metadata(hash)
@id = hash['id']
@menu_title = hash['menu_title']
@path = hash['path']
@method = hash['method']
end
end
class ResourceDesc
include RenderAsSection
include Anchorable
attr_reader :resource_name
def initialize(resource_name, content)
@resource_name = resource_name
@content = content
end
def name
'desc'
end
def anchor_id
"#{resource_name}-desc"
end
def generate_example(sections)
endpoints = sections.reject { |s| s.name == 'desc' || s.name == 'object' }
signatures = endpoints.each_with_object("") do |e, res|
res << %(#{e.signature}
)
end
@example = <<-HTML
HTML
end
def example_to_html
@example
end
end
class Resource
DEFAULT = %w[desc object index show create update destroy]
def self.build(name, paths, endpoints: nil)
endpoints ||= DEFAULT
desc = nil
sections = endpoints.each_with_object([]) do |endpoint, res|
filename = paths[endpoint]
next unless filename
content = File.read(filename)
if endpoint == 'desc'
desc = ResourceDesc.new(name, content)
res << desc
else
res << Section.new(endpoint, content, resource: name)
end
end
desc&.generate_example(sections)
Resource.new(name, sections)
end
attr_reader :name, :sections
def initialize(name, sections)
@name, @sections = name, sections
end
def output
head, *tail = sections
head.include_show_button = true
inner = sections.map(&:output).join("\n")
%()
end
def menu_output
head, *tail = sections
<<-HTML
#{head.anchor(name.capitalize, class_list: 'expandable')}
#{tail.map(&:menu_output).join}
HTML
end
end
class Generator
attr_reader :renderer, :config
def initialize(config_path)
@config = YAML.load_file(config_path)
@content_dir = @config['content_dir']
@parts = {}
end
def run
tree = build_content_tree
nodes = config['resources'].map do |rs|
if rs.is_a?(Hash)
name, endpoints = rs.each_pair.first
paths = tree[name]
Resource.build(name, paths, endpoints: endpoints)
else
paths = tree[rs]
paths.is_a?(Hash) ? Resource.build(rs, paths) : Section.new(rs, File.read(paths))
end
end
out = File.new("#{Dir.tmpdir}/rtfdoc_output.html", 'w')
out.write(Template.new(nodes).output)
out.close
end
private
def build_content_tree
tree = {}
slicer = (@content_dir.length + 1)..-1
ext_slicer = -3..-1
Dir.glob("#{@content_dir}/**/*.md").each do |path|
str = path.slice(slicer)
parts = str.split('/')
filename = parts.pop
filename.slice!(ext_slicer)
leaf = parts.reduce(tree) { |h, part| h[part] || h[part] = {} }
leaf[filename] = path
end
tree
end
end
def self.renderer
@renderer ||= Redcarpet::Markdown.new(Renderer, {
underline: true,
space_after_headers: true,
fenced_code_blocks: true,
no_intra_emphasis: true,
tables: true
})
end
def self.markdown_to_html(text)
renderer.render(text)
end
end