class HTML < Generic

Inherits from
Generic: #out, #render, #reference_def, #cr, #block, #container, #plain

Renders Markdown node trees as HTML.

Definitions

def initialize(ids: false, headings: nil, tight: false, **options)

Initializes an HTML renderer.

Signature

parameter ids Boolean

Whether to wrap headings in anchored sections.

parameter headings Markly::Renderer::Headings | Nil

A heading tracker to reuse.

parameter tight Boolean

Whether to render paragraphs tightly.

option :flags Integer

The enabled rendering flags.

option :extensions Array(Symbol)

The enabled extensions.

Implementation

def initialize(ids: false, headings: nil, tight: false, **options)
	super(**options)
	
	# Initialize heading tracker if IDs are enabled
	@headings = headings || (ids ? Headings.new : nil)
	
	@section = nil
	@tight = tight
	
	@footnotes = {}
end

def document(_)

Renders a complete document and closes any generated sections.

Signature

parameter _ Markly::Node

The document node.

Implementation

def document(_)
	@section = false
	super
	out("</ol>\n</section>\n") if @written_footnote_ix
	out("</section>") if @section
end

def id_for(node)

Returns an escaped HTML id attribute for a heading node.

Signature

parameter node Markly::Node

The heading node.

returns String | Nil

The id attribute when heading IDs are enabled.

Implementation

def id_for(node)
	if @headings
		anchor = @headings.anchor_for(node)
		# `CGI.escape_html` is not exposed by `cgi/escape` on Ruby 3.4:
		return " id=\"#{CGI.escapeHTML anchor}\""
	end
end

def self.anchor_for(node)

Generates a normalized anchor from a node's plain-text content.

Signature

parameter node Markly::Node

The node to convert.

returns String

The normalized anchor.

Implementation

def self.anchor_for(node)
	# Convert to plaintext, strip trailing whitespace, convert to lowercase:
	text = node.to_plaintext.chomp.downcase
	
	# Replace sequences of whitespace with hyphens:
	text.gsub!(/\s+/, "-")
	
	return text
end

def anchor_for(node)

Generates a normalized anchor from a node's plain-text content.

Signature

parameter node Markly::Node

The node to convert.

returns String

The normalized anchor.

Implementation

def anchor_for(node)
	self.class.anchor_for(node)
end

def header(node)

Renders a heading node, optionally wrapped in an anchored section.

Signature

parameter node Markly::Node

The heading node.

Implementation

def header(node)
	block do
		if @headings
			out("</section>") if @section
			@section = true
			out("<section#{id_for(node)}>")
		end
		
		out("<h", node.header_level, "#{source_position(node)}>", :children, "</h", node.header_level, ">")
	end
end

def paragraph(node)

Renders a paragraph node.

Signature

parameter node Markly::Node

The paragraph node.

Implementation

def paragraph(node)
	if @tight && node.parent.type != :blockquote
		out(:children)
	else
		block do
			container("<p#{source_position(node)}>", "</p>") do
				out(:children)
				if node.parent.type == :footnote_definition && node.next.nil?
					out(" ")
					out_footnote_backref
				end
			end
		end
	end
end

def list(node)

Renders an ordered or unordered list node.

Signature

parameter node Markly::Node

The list node.

Implementation

def list(node)
	old_tight = @tight
	@tight = node.list_tight
	
	block do
		if node.list_type == :bullet_list
			container("<ul#{source_position(node)}>\n", "</ul>") do
				out(:children)
			end
		else
			start = if node.list_start == 1
				"<ol#{source_position(node)}>\n"
			else
				"<ol start=\"#{node.list_start}\"#{source_position(node)}>\n"
			end
			container(start, "</ol>") do
				out(:children)
			end
		end
	end
	
	@tight = old_tight
end

def list_item(node)

Renders a list-item node, including task-list attributes when present.

Signature

parameter node Markly::Node

The list-item node.

Implementation

def list_item(node)
	block do
		tasklist_data = tasklist(node)
		container("<li#{source_position(node)}#{tasklist_data}>#{' ' if tasklist?(node)}", "</li>") do
			out(:children)
		end
	end
end

def tasklist(node)

Returns the HTML fragment required for a task-list item.

Signature

parameter node Markly::Node

The list-item node.

returns String

The task-list fragment, or an empty string.

Implementation

def tasklist(node)
	return "" unless tasklist?(node)
	
	state = if checked?(node)
		'checked="" disabled=""'
	else
		'disabled=""'
	end
	"><input type=\"checkbox\" #{state} /"
end

def blockquote(node)

Renders a blockquote node.

Signature

parameter node Markly::Node

The blockquote node.

Implementation

def blockquote(node)
	block do
		container("<blockquote#{source_position(node)}>\n", "</blockquote>") do
			out(:children)
		end
	end
end

def hrule(node)

Renders a thematic-break node.

Signature

parameter node Markly::Node

The thematic-break node.

Implementation

def hrule(node)
	block do
		out("<hr#{source_position(node)} />")
	end
end

def code_block(node)

Renders a code block and its optional language metadata.

Signature

parameter node Markly::Node

The code block node.

Implementation

def code_block(node)
	block do
		language = node.code_language
		
		if flag_enabled?(GITHUB_PRE_LANG)
			out("<pre#{source_position(node)}")
			out(' lang="', language, '"') if language
			out("><code>")
		else
			out("<pre#{source_position(node)}><code")
			if language
				out(' class="language-', language, '">')
			else
				out(">")
			end
		end
		out(escape_html(node.string_content))
		out("</code></pre>")
	end
end

def html(node)

Renders or omits a raw block-level HTML node according to the flags.

Signature

parameter node Markly::Node

The raw HTML node.

Implementation

def html(node)
	block do
		if flag_enabled?(UNSAFE)
			out(tagfilter(node.string_content))
		else
			out("<!-- raw HTML omitted -->")
		end
	end
end

def inline_html(node)

Renders or omits a raw inline HTML node according to the flags.

Signature

parameter node Markly::Node

The raw inline HTML node.

Implementation

def inline_html(node)
	if flag_enabled?(UNSAFE)
		out(tagfilter(node.string_content))
	else
		out("<!-- raw HTML omitted -->")
	end
end

def emph(node)

Renders an emphasized inline node.

Signature

parameter node Markly::Node

The emphasized node.

Implementation

def emph(node)
	out("<em>", :children, "</em>")
end

def strong(node)

Renders a strongly emphasized inline node.

Signature

parameter node Markly::Node

The strong node.

Implementation

def strong(node)
	if node.parent.nil? || node.parent.type == node.type
		out(:children)
	else
		out("<strong>", :children, "</strong>")
	end
end

def image(node)

Renders an image node with plain-text alternative content.

Signature

parameter node Markly::Node

The image node.

Implementation

def image(node)
	out('<img src="', escape_href(node.url), '"')
	plain do
		out(' alt="', :children, '"')
	end
	out(' title="', escape_html(node.title), '"') if node.title && !node.title.empty?
	out(" />")
end

def text(node)

Renders an escaped text node.

Signature

parameter node Markly::Node

The text node.

Implementation

def text(node)
	out(escape_html(node.string_content))
end

def code(node)

Renders an inline code node and its optional language metadata.

Signature

parameter node Markly::Node

The inline code node.

Implementation

def code(node)
	language = node.code_language
	out("<code")
	out(' class="language-', language, '"') if language
	out(">")
	out(escape_html(node.string_content))
	out("</code>")
end

def linebreak(_node)

Renders a hard line break.

Signature

parameter _node Markly::Node

The line-break node.

Implementation

def linebreak(_node)
	out("<br />\n")
end

def softbreak(_)

Renders a soft line break according to the configured flags.

Signature

parameter _ Markly::Node

The soft-break node.

Implementation

def softbreak(_)
	if flag_enabled?(HARD_BREAKS)
		out("<br />\n")
	elsif flag_enabled?(NO_BREAKS)
		out(" ")
	else
		out("\n")
	end
end

def table(node)

Renders a table node and initializes its column alignments.

Signature

parameter node Markly::Node

The table node.

Implementation

def table(node)
	@alignments = node.table_alignments
	@needs_close_tbody = false
	out("<table#{source_position(node)}>\n", :children)
	out("</tbody>\n") if @needs_close_tbody
	out("</table>\n")
end

def table_header(node)

Renders a table-header row.

Signature

parameter node Markly::Node

The table-header node.

Implementation

def table_header(node)
	@column_index = 0
	
	@in_header = true
	out("<thead>\n<tr#{source_position(node)}>\n", :children, "</tr>\n</thead>\n")
	@in_header = false
end

def table_row(node)

Renders a table row, opening the table body when necessary.

Signature

parameter node Markly::Node

The table-row node.

Implementation

def table_row(node)
	@column_index = 0
	if !@in_header && !@needs_close_tbody
		@needs_close_tbody = true
		out("<tbody>\n")
	end
	out("<tr#{source_position(node)}>\n", :children, "</tr>\n")
end

TABLE_CELL_ALIGNMENT

Signature

constant

HTML attributes for table-cell alignments.

Implementation

TABLE_CELL_ALIGNMENT = {
	left: ' align="left"',
	right: ' align="right"',
	center: ' align="center"'
}.freeze

def table_cell(node)

Renders a table cell using the current column alignment.

Signature

parameter node Markly::Node

The table-cell node.

Implementation

def table_cell(node)
	align = TABLE_CELL_ALIGNMENT.fetch(@alignments[@column_index], "")
	out(@in_header ? "<th#{align}#{source_position(node)}>" : "<td#{align}#{source_position(node)}>", :children, @in_header ? "</th>\n" : "</td>\n")
	@column_index += 1
end

def strikethrough(_)

Renders a strikethrough node.

Signature

parameter _ Markly::Node

The strikethrough node.

Implementation

def strikethrough(_)
	out("<del>", :children, "</del>")
end

def footnote_reference(node)

Renders a footnote reference linking to its definition.

Signature

parameter node Markly::Node

The footnote-reference node.

Implementation

def footnote_reference(node)
	label = node.parent_footnote_def.string_content
	
	out("<sup class=\"footnote-ref\"><a href=\"#fn-#{label}\" id=\"fnref-#{label}\" data-footnote-ref>#{node.string_content}</a></sup>")
	# out(node.to_html)
end

def footnote_definition(node)

Renders a footnote definition and records its backlink target.

Signature

parameter node Markly::Node

The footnote-definition node.

Implementation

def footnote_definition(node)
	unless @footnote_ix
		out("<section class=\"footnotes\" data-footnotes>\n<ol>\n")
		@footnote_ix = 0
	end
	
	@footnote_ix += 1
	label = node.string_content
	@footnotes[@footnote_ix] = label
	
	out("<li id=\"fn-#{label}\">\n", :children)
	out("\n") if out_footnote_backref
	out("</li>\n")
	# </ol>
	# </section>
end