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
pathString 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
documentMarkly::Node The document to expand in-place.
-
parameter
base_dirString Directory used to resolve relative paths.
-
parameter
depthInteger 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
documentMarkly::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