PresentlySourcePresentlySlideParser

module Parser

Parses a Markdown slide file into structured data for class Presently::Slide.

Handles YAML front_matter extraction, presenter note separation, and Markdown AST construction via Markly.

Definitions

def load(path)

Parse the file and return a class Presently::Slide.

Signature

parameter path String

The file path to parse.

returns Slide

Implementation

def load(path)
	raw = File.read(path)
	
	# Parse once, with native front matter support.
	document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: Fragment::EXTENSIONS)
	
	expand_includes!(document, File.dirname(path))
	
	# Extract front matter from the first AST node if present.
	front_matter = nil
	if (front_matter_node = document.first_child) && front_matter_node.type == :front_matter
		front_matter = YAML.safe_load(front_matter_node.string_content)
		front_matter_node.delete
	end
	
	# Find the last hrule, which acts as the separator between slide content and presenter notes.
	last_hrule = nil
	document.each{|node| last_hrule = node if node.type == :hrule}
	
	if last_hrule
		notes_node = Markly::Node.new(:document)
		while child = last_hrule.next
			notes_node.append_child(child)
		end
		last_hrule.delete
		
		# Extract the last javascript code block from the notes as the slide script.
		script_node = nil
		notes_node.each do |node|
			if node.type == :code_block && node.fence_info.to_s.strip == "javascript"
				script_node = node
			end
		end
		
		script = nil
		if script_node
			script = script_node.string_content
			script_node.delete
		end
		
		content = parse_sections(document)
		notes = Fragment.new(notes_node)
	else
		content = parse_sections(document)
		notes = nil
		script = nil
	end
	
	Slide.new(path, front_matter: front_matter, content: content, notes: notes, script: script)
end

def expand_includes!(document, base_dir, depth: 0)

Expand ![[path/to/file.md]] include directives in a parsed document.

Scans top-level paragraph nodes for the Obsidian-style embed syntax and replaces each one with the parsed AST of the referenced file. Includes are resolved relative to base_dir. Front matter in included files is stripped. Nested includes are expanded recursively up to a depth of 10.

Signature

parameter document Markly::Node

The document to expand in-place.

parameter base_dir String

Directory used to resolve relative paths.

parameter depth Integer

Current recursion depth (guards against cycles).

Implementation

def expand_includes!(document, base_dir, depth: 0)
	raise "Include depth limit exceeded" if depth > 10
	
	# Collect matching paragraphs first — mutating the tree while iterating is unsafe.
	to_replace = []
	document.each do |node|
		next unless node.type == :paragraph
		child = node.first_child
		next unless child && child.next.nil? && child.type == :text
		next unless child.string_content =~ /\A!\[\[(.+?)\]\]\z/
		to_replace << [node, $1.strip]
	end
	
	to_replace.each do |paragraph, relative_path|
		included_path = File.expand_path(relative_path, base_dir)
		included_raw = File.read(included_path)
		included_document = Markly.parse(included_raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: Fragment::EXTENSIONS)
		
		# Strip front matter from included file if present.
		front_matter_node = included_document.first_child
		if front_matter_node&.type == :front_matter
			front_matter_node.delete
		end
		
		expand_includes!(included_document, File.dirname(included_path), depth: depth + 1)
		
		included_document.each{|node| paragraph.insert_before(node.dup)}
		paragraph.delete
	end
end

def parse_sections(document)

Parse a Markly document into content sections based on top-level headings.

Each heading becomes a named key; content before the first heading is collected under "body". Each value is a class Presently::Slide::Fragment wrapping a document node.

Signature

parameter document Markly::Node

The document to parse.

returns Hash(String, Fragment)

Sections keyed by heading name.

Implementation

def parse_sections(document)
	sections = {}
	current_key = "body"
	current_node = Markly::Node.new(:document)
	
	document.each do |node|
		if node.type == :header
			sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
			current_key = node.to_plaintext.strip.downcase.gsub(/\s+/, "_")
			current_node = Markly::Node.new(:document)
		else
			current_node.append_child(node.dup)
		end
	end
	
	sections[current_key] = Fragment.new(current_node) unless current_node.first_child.nil?
	
	sections
end