require 'erubi'
require 'rouge'
require 'redcarpet'
require 'tmpdir'
require 'rtfdoc/version'
module RTFDoc
class AttributesComponent
# Needed because we can't call the same rendered within itself.
def self.private_renderer
@renderer ||= Redcarpet::Markdown.new(::RTFDoc::Renderer, {
underline: true,
space_after_headers: true,
fenced_code_blocks: true,
no_intra_emphasis: true
})
end
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
def to_html(text)
self.class.private_renderer.render(text) if text
end
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 link(link, title, content)
%(#{content})
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'
format_code('RESPONSE', code)
elsif language == 'title_and_code'
title, _code = code.split("\n", 2)
title ||= 'RESPONSE'
format_code(title, _code)
end
end
private def format_code(title, code)
<<-HTML
#{title}
#{rouge_formatter.format(rouge_lexer.lex(code.strip))}
HTML
end
end
class Template
attr_reader :app_name, :page_title
def initialize(nodes, config)
@content = nodes.flat_map(&:output).join
# @menu_content = nodes.map(&:menu_output).join
@app_name = config['app_name']
@page_title = config['title']
generate_grouped_menu_content(nodes)
end
def output
template = Erubi::Engine.new(File.read(File.expand_path('../src/index.html.erb', __dir__)))
eval(template.src)
end
private
# Transform a list of nodes into a list of groups. If all nodes already are groups, it will
# return the same list. Otherwise, it will build group from consecutives single resources.
def generate_grouped_menu_content(nodes)
i = 0
res = []
while i < nodes.length
node = nodes[i]
if node.is_a?(Group)
res << node
i += 1
else
inner = []
j = i
while node && !node.is_a?(Group)
inner << node
j += 1
node = nodes[j]
end
res << Group.new(nil, inner)
i = j
end
end
@menu_content = res.map(&:menu_output).join
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 && !meta_section? && !@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
anchor(sig)
end
def example_to_html
res = super
@resource && res && !meta_section? ? res.sub('RESPONSE', sig) : res
end
private
def sig
@sig ||= <<-HTML.strip!
HTML
end
def meta_section?
name == 'desc' || name == 'object'
end
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.is_a?(Scope) || s.name == 'desc' || s.name == 'object' }
signatures = endpoints.each_with_object("") do |e, res|
res << %(#{e.signature}
)
end
scopes = sections.select { |s| s.is_a?(Scope) }.map!(&:generate_example).join("\n")
@example = <<-HTML
#{scopes}
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|
if endpoint.is_a?(Hash)
n, values = endpoint.each_pair.first
next unless n.start_with?('scope|')
dir_name = n.slice(6..-1)
scope_name = values['title'] || dir_name
scoped_endpoints = values['endpoints']
subsections = scoped_endpoints.each_with_object([]) do |e, r|
filename = paths.dig(dir_name, e)
next unless filename
content = File.read(filename)
r << Section.new(e, content, resource: name)
end
res << Scope.new(scope_name, subsections)
next res
end
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.flat_map(&:output).join("\n")
%()
end
def menu_output
head, *tail = sections
<<-HTML
#{head.anchor(human_name, class_list: 'expandable')}
#{tail.map(&:menu_output).join}
HTML
end
private
def human_name
name.tr('_', ' ').split.map!(&:capitalize).join(' ')
end
end
class Group
attr_reader :name, :resources
def initialize(name, resources, options = {})
@name = name
@resources = resources
sorted = true
sorted = options['sort'] if options.key?('sort')
@resources.sort! { |a, b| a.name <=> b.name } if sorted
end
def output
resources.map(&:output)
end
def menu_output
title = "#{name}
" if name && name.length > 0
<<-HTML
HTML
end
end
class Scope
attr_reader :name, :sections
def initialize(name, sections)
@name = name
@sections = sections
end
def output
sections.map(&:output)
end
def menu_output
<<-HTML
#{name}
#{sections.map(&:menu_output).join}
HTML
end
def generate_example
signatures = sections.each_with_object("") do |s, res|
res << %(#{s.signature}
)
end
<<-HTML
#{name} ENDPOINTS
#{signatures}
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 = build_nodes(config['resources'])
out = File.new("#{Dir.tmpdir}/rtfdoc_output.html", 'w')
out.write(Template.new(nodes, config).output)
out.close
end
private
def build_nodes(ary, allow_groups: true)
ary.map do |rs|
if rs.is_a?(Hash)
name, values = rs.each_pair.first
if name.start_with?('group|')
raise 'Nested groups are not yet supported' if !allow_groups
group_name = values.key?('title') ? values['title'] : name.slice(6..-1)
Group.new(group_name, build_nodes(values['resources'], allow_groups: false), values)
else
paths = @tree[name]
Resource.build(name, paths, endpoints: values)
end
else
paths = @tree[rs]
paths.is_a?(Hash) ? Resource.build(rs, paths) : Section.new(rs, File.read(paths))
end
end
end
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