diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e0184..2fd3b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - Rename `Obsidian::MarkdownConverter` to `Obsidian::ParsedMarkdownDocument` +- Unify `Note` and `Index` classes into `Page`. This is a breaking API change. ## [0.3.0] - 2023-07-27 diff --git a/lib/obsidian/parser.rb b/lib/obsidian/parser.rb index b516334..070823a 100644 --- a/lib/obsidian/parser.rb +++ b/lib/obsidian/parser.rb @@ -3,6 +3,7 @@ require_relative "parser/version" require_relative "parser/parsed_markdown_document" require_relative "parser/obsidian_flavored_markdown" +require_relative "parser/page" module Obsidian class Error < StandardError; end diff --git a/lib/obsidian/parser/page.rb b/lib/obsidian/parser/page.rb new file mode 100644 index 0000000..5570c82 --- /dev/null +++ b/lib/obsidian/parser/page.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Obsidian + # A page in the vault corresponding to either a markdown document, + # or a directory containing other documents. + # + # If a directory contains an index.md, that is used as the content of + # the directory page; otherwise content will be nil. + class Page + def self.create_root + Page.new(title: "", slug: "") + end + + def initialize(title:, slug:, last_modified: nil, content: nil, parent: nil) + # TODO: check frontmatter for titles as well + @title = title + @slug = slug + @last_modified = last_modified + @content = content + @parent = parent + @children = {} + end + + def is_index? + !children.empty? + end + + def inspect + "Page(title: #{title.inspect}, slug: #{slug.inspect})" + end + + def ==(other) + !slug.nil? && slug == other.slug + end + + alias_method :eql?, :== + + def hash + slug.hash + end + + # Add a note to the tree based on its slug. + # Call this method on the root page. + # When calling this method, you must ensure that anscestor pages + # are added before their descendents. + def add_page(slug, last_modified: nil, content: nil) + path_components = slug.split("/") + raise ArgumentError, "Expecting non-empty slug" if path_components.empty? + + title = path_components.pop + + parent = path_components.reduce(self) do |index, anscestor_title| + anscestor_slug = Obsidian.build_slug(anscestor_title, index.slug) + index.get_or_create_child(slug: anscestor_slug, title: anscestor_title) + end + + parent.get_or_create_child( + title: title, + slug: slug, + last_modified: last_modified, + content: content + ) + end + + def get_or_create_child(title:, slug:, last_modified: nil, content: nil) + # TODO: validate slug matches the current page slug + + @children[title] ||= Page.new( + slug: slug, + title: title, + last_modified: last_modified, + content: content, + parent: self + ) + end + + def children + @children.values.sort_by { |c| [c.is_index? ? 1 : 0, c.title] } + end + + attr_reader :title + attr_reader :slug + attr_reader :last_modified + attr_reader :content + attr_reader :parent + end +end diff --git a/spec/obsidian/parser/page_spec.rb b/spec/obsidian/parser/page_spec.rb new file mode 100644 index 0000000..ccda4fc --- /dev/null +++ b/spec/obsidian/parser/page_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +RSpec.describe Obsidian::Page do + subject(:root) { described_class.create_root } + + describe("#add_page") do + it "relates too pages" do + page = root.add_page("foo") + + expect(page.parent).to eq(root) + expect(root.children).to eq([page]) + end + + it "assigns titles and slugs to a top level page" do + page = root.add_page("foo") + + expect(page.slug).to eq("foo") + expect(page.title).to eq("foo") + end + + it "assigns titles and slugs to a nested page" do + page = root.add_page("foo/bar/baz") + + expect(page.slug).to eq("foo/bar/baz") + expect(page.title).to eq("baz") + end + + it "infers missing directory pages" do + page = root.add_page("foo/bar/baz") + parent = page.parent + grandparent = parent.parent + + expect(parent.slug).to eq("foo/bar") + expect(grandparent.slug).to eq("foo") + expect(parent.children).to eq([page]) + expect(grandparent.children).to eq([parent]) + expect(root.children).to eq([grandparent]) + end + end +end