wikicr/src/lib/page/index.cr

159 lines
5.3 KiB
Crystal

require "yaml"
require "./index/entry"
# And Index is an object that associate a file with a lot of meta-data
# like related url, the title, the table of content, ...
class Wikicr::Page
class Index < Lockable
alias Entries = Hash(String, Entry) # path, entry
include YAML::Serializable
property file : String
property entries : Entries
# property find_index : Hash(String, String)
# property find_tags : Hash(String, Array(String))
def initialize(@file : String)
@entries = Entries.new
# @find_index = of String => String
# @find_tags = of String => Array(String)
end
# Find a matching *text* into the Index.
# If no matching content, return a default value.
def one_by_title_or_url(text : String, context : Page, raise_not_found : Bool = false) : Index::Entry
found = find_by_title(text, context) || find_by_url(text, context)
if found.nil?
raise "Page not found" if raise_not_found
STDERR.puts "warning: no page \"#{text}\" found"
Index::Entry.from_context(context, text)
else
return found
end
end
# Find a specific url into the Index.
# If no matching content, return a default value.
# If the page is not found, the title of the entry will be the default_title or the url
def one_by_url(url : String, context : Page, default_title : String? = nil, raise_not_found : Bool = false) : Index::Entry
found = find_by_url(url, context)
if found.nil?
raise "Page not found" if raise_not_found
STDERR.puts "warning: no page \"#{url}\" found"
title = default_title || url
Index::Entry.from_context(context, title, url)
else
return found
end
end
TAG_SIGN_REQUIRE = '+'
TAG_SIGN_FORBIDE = '-'
TAG_SIGNS = {
TAG_SIGN_REQUIRE,
TAG_SIGN_FORBIDE,
}
# Find a matching *text* into the Index.
# If no matching content, return a default value.
# @param tag_line must be a "tag +andtagx -butnottagy ortag3"
def all_by_tags(tags_line : String, context : Page) : Entries
tags = tags_line.split(' ')
required_tags = tags.select { |tag| tag[0] == TAG_SIGN_REQUIRE }.map { |tag| tag[1..-1] }
forbidden_tags = tags.select { |tag| tag[0] == TAG_SIGN_FORBIDE }.map { |tag| tag[1..-1] }
at_least_one_tags = tags.select { |tag| !TAG_SIGNS.includes? tag[0] }
@entries.select do |url, entry|
(entry.tags & required_tags).size == required_tags.size &&
(entry.tags & forbidden_tags).size == 0 &&
(entry.tags & at_least_one_tags).size > 0
end
end
# Find the closest `Index`' `Entry` to *text* based on the entries title
# and searching for the closer url as possible to the context
private def find_by_title(text : String, context : Page) : Entry?
# exact_matched = @entries.select{|_, entry| entry.title == text }.values
# return choose_closer_url(exact_matched, context) unless exact_matched.empty?
slug_matched = @entries.select { |_, entry|
entry.slug == Index::Entry.title_to_slug(text)
}.values
return choose_closer_url(slug_matched, context) unless slug_matched.empty?
nil
end
# Find the url which is the closest as possible than the context url (start with the maxmimum common chars).
private def choose_closer_url(entries : Array(Entry), context : Page) : Entry
raise "Cannot handle empty array" if entries.empty?
entries.reduce { |lhs, rhs| Index.url_closeness(context.url, lhs.url) >= Index.url_closeness(context.url, rhs.url) ? lhs : rhs }
end
# Computes the amount of common chars at the beginning of each string
def self.url_closeness(from : String, to : String)
from.size.times do |i|
return i if from[i] != to[i]
end
return from.size
end
private def find_by_url(text : String, context : Page) : Entry?
slug_matched = @entries.select { |_, entry|
entry.url == Index::Entry.title_to_slug(text) ||
entry.url == File.join(context.url_dirname, Index::Entry.title_to_slug(text))
}.values
return choose_closer_url(slug_matched, context) unless slug_matched.empty?
nil
end
# Access to an existing `Entry`.
def [](page : Wikicr::Page) : Index::Entry
@entries[page.path]
end
# Access to an existing `Entry`.
def []?(page : Wikicr::Page) : Index::Entry?
@entries[page.path]?
end
# Add a new `Entry`.
def add(page : Wikicr::Page)
@entries[page.path] = Entry.new(
path: page.path,
url: page.url,
title: page.title,
read_toc: true,
tags: page.tags,
)
self
end
# Remove an `Entry` from the `Index` based on its path.
def delete(page : Wikicr::Page)
@entries.delete page.path
self
end
# Replace the old Index using the state registrated into the *file*.
def load!
if File.exists?(@file) && (new_index = Index.read(@file) rescue nil)
@entries = new_index.entries
# @file = index.file
else
@entries = {} of String => Entry
end
self
end
def self.read(file : String)
Index.from_yaml File.read(file)
end
# Save the current state into the file
def save!
File.write @file, self.to_yaml
self
end
end
end