Module: Presently::Slide::Parser

Defined in:
lib/presently/slide.rb

Overview

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

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

Class Method Summary collapse

Class Method Details

.expand_includes!(document, base_dir, depth: 0) ⇒ Object

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.



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/presently/slide.rb', line 127

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

.load(path) ⇒ Object

Parse the file and return a Presently::Slide.



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/presently/slide.rb', line 66

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

.parse_sections(document) ⇒ Object

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 Fragment wrapping a document node.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/presently/slide.rb', line 164

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