# lib/jekyll/obsidian.rb # frozen_string_literal: true require "jekyll" require "json" require "fileutils" require_relative "obsidian/version" module Jekyll module Obsidian # Jekyll::Hooks.register :site, :post_write do |site| # vault = site.config["obsidian_vault"] # vault_path = File.join(site.dest, vault) # Dir.glob(File.join(vault_path, "**", "*.md")).each do |md_file| # new_file_path = md_file.sub(/\.md$/, ".") # File.rename(md_file, new_file_path) # end # end class FileTreeGenerator < Jekyll::Generator safe true priority :lowest def generate(site) # -------------------------------- site config ------------------------------- # vault = site.config["obsidian_vault"] || site.source if vault.nil? puts "Error: obsidian_vault is not set in config.yml" exit(1) end enable_backlinks = site.config["obsidian_backlinks"] enable_embeds = site.config["obsidian_embeds"] # --------------------------------- site data -------------------------------- # data_dir = File.join(File.dirname(site.dest), "_data", "obsidian") FileUtils.mkdir_p(data_dir) unless File.directory?(data_dir) site.data["obsidian"] = {} unless site.data["obsidian"] counts = {dirs: 0, files: 0, size: 0} obsidian_files = collect_files(vault, "", counts) vault_data_json = File.join(data_dir, "vault_data.json") File.write(vault_data_json, JSON.pretty_generate(counts.to_json)) vault_files_json = File.join(data_dir, "vault_files.json") File.write(vault_files_json, JSON.pretty_generate(obsidian_files.to_json)) backlinks, embeds = build_links(vault, obsidian_files, obsidian_files) if enable_backlinks || enable_backlinks.nil? backlinks_json = File.join(data_dir, "backlinks.json") File.write(backlinks_json, escape_backlinks(backlinks)) puts "Backlinks built." else puts "Backlinks disabled" end if enable_embeds || enable_embeds.nil? embeds_json = File.join(data_dir, "embeds.json") File.write(embeds_json, escape_embeds(embeds)) puts "Embeds built." else puts "Embeds disabled" end site.config["obsidian_homepage"] obsidian_dir = File.join(File.dirname(site.dest), "_includes", "obsidian") FileUtils.mkdir_p(obsidian_dir) unless File.directory?(obsidian_dir) layouts_dir = File.join(File.dirname(site.dest), "_layouts") FileUtils.mkdir_p(layouts_dir) unless File.directory?(layouts_dir) scss_dir = File.join(File.dirname(site.dest), "assets", "obsidian") FileUtils.mkdir_p(scss_dir) unless File.directory?(scss_dir) partials_dir = File.join(File.dirname(site.dest), "_sass", "obsidian") FileUtils.mkdir_p(partials_dir) unless File.directory?(partials_dir) project_root = File.expand_path("../..", File.dirname(__FILE__)) plugin_dir = File.join(project_root, "assets") main_scss = File.join(plugin_dir, "css", "obsidian.scss") copy_file_to_dir(main_scss, scss_dir) copy_files_from_dir(File.join(plugin_dir, "css", "partials"), partials_dir) layout = File.join(plugin_dir, "layouts", "obsidian.html") copy_file_to_dir(layout, layouts_dir, true) copy_files_from_dir(File.join(plugin_dir, "includes"), obsidian_dir, true) end private def copy_file_to_dir(file, dir, overwrite = false) if File.exist?(file) destination_file = File.join(dir, File.basename(file)) if !overwrite && File.exist?(destination_file) puts "#{File.basename(file)} currently exists" else FileUtils.cp(file, dir) # puts "#{File.basename(file)} copied over" end else puts "Error: #{file} does not exist" exit end end def copy_files_from_dir(source_dir, destination_dir, overwrite = false) Dir.glob(File.join(source_dir, "*")).each do |file_path| next if File.directory?(file_path) copy_file_to_dir(file_path, destination_dir, overwrite) end end def excluded_file_exts(filename) extensions = [".exe", ".bat", ".sh", ".zip", ".7z", ".stl", ".fbx"] is_excluded = extensions.any? { |ext| filename.end_with?(ext) } if is_excluded puts "Excluded file: #{filename}" end is_excluded end # ------------------------ Ruby Hash object generators ----------------------- # def collect_files(rootdir, path = "", counts = {dirs: 0, files: 0, size: 0}) root_files_ = [] Dir.entries(rootdir).each do |entry| next if entry.start_with?(".", "_") entry_path = File.join(rootdir, entry) root_files_ << if File.directory?(entry_path) next if entry.start_with?(".obsidian") counts[:dirs] += 1 {name: entry, type: "dir", path: File.join(path, entry), children: collect_files(entry_path, File.join(path, entry), counts)} else next if File.zero?(entry_path) || File.empty?(entry_path) if /\.\./.match?(entry) # Checks for two or more consecutive periods base_name = File.basename(entry, File.extname(entry)).gsub(/\.{2,}/, ".") base_name = base_name.chomp(".") trimmed_name = base_name.chomp(".") + File.extname(entry) puts "file path: #{trimmed_name} #{entry}" if trimmed_name != entry new_path = File.join(rootdir, trimmed_name) File.rename(entry_path, new_path) entry_path = new_path entry = trimmed_name end end counts[:files] += 1 counts[:size] += File.size(entry_path) {name: entry, type: "file", path: File.join(path, entry), size: File.size(entry_path)} end end root_files_ end def build_links(rootdir, root_files_, root_files, backlinks = {}, embeds = {}) root_files_.each do |file| if file[:type] == "dir" build_links(rootdir, file[:children], root_files, backlinks, embeds) elsif file[:type] == "file" entry_path = File.join(rootdir, file[:path]) next if File.zero?(entry_path) || excluded_file_exts(file[:name]) if file[:name].end_with?(".md", ".canvas") begin content = File.read(entry_path) updated = false rescue Errno::ENOENT puts "Error reading file: #{entry_path} - No such file" next rescue Errno::EACCES puts "Error reading file: #{entry_path} - Permission denied" next end links = content.scan(/\[\[(.*?)\]\]/).flatten backlinks[file[:path]] ||= {"backlink_paths" => []} links.each do |link| matched_entry = find_matching_entry(root_files, link.downcase) if matched_entry unless matched_entry[:path] == file[:path] || backlinks[file[:path]]["backlink_paths"].include?(matched_entry[:path]) backlinks[file[:path]]["backlink_paths"] << matched_entry[:path] end end end embeds_ = content.scan(/!\[\[(.*?)\]\]/).flatten embeds_.each do |embed| if /\.\./.match?(embed) base_name = File.basename(embed, File.extname(embed)).gsub(/\.\.+/, ".") base_name = base_name.chomp(".") trimmed_name = base_name + File.extname(embed) content = content.gsub("![[#{embed}]]", "![[#{trimmed_name}]]") puts("trimmed_name #{trimmed_name} #{content}") updated = true end end if updated File.write(entry_path, content) puts "Updated file: #{entry_path}" end elsif embeds[file[:path]].nil? || embeds[file[:path]]["embed_paths"].nil? embeds[file[:path]] = {"embed_paths" => [entry_path]} else unless embeds[file[:path]]["embed_paths"].include?(entry_path) embeds[file[:path]]["embed_paths"] << entry_path end end else puts "Skipping non-markdown file: #{file[:name]}" end end [backlinks, embeds] end def find_matching_entry(files, lowercase_link) stripped_link = lowercase_link.sub(/\|.*$/, "").sub(/#.*$/, "") files.each do |file| if file[:type] == "dir" result = find_matching_entry(file[:children], lowercase_link) return result if result elsif file[:type] == "file" && file[:name].end_with?(".md", ".canvas") file_name_without_extension = file[:name].sub(/\.\w+$/, "").downcase return file if file_name_without_extension == stripped_link end end nil end # ------------------------ Ruby Hash object formatters ----------------------- # def escape_backlinks(backlinks) escaped_backlinks = {} backlinks.each do |path, data| escaped_path = escape_path(path) escaped_data = { "backlink_paths" => data["backlink_paths"].map do |path| escape_path(path) end } escaped_backlinks[escaped_path] = escaped_data end JSON.pretty_generate(escaped_backlinks.to_json) end def escape_embeds(embeds) escaped_embeds = {} embeds.each do |path, _| escaped_path = escape_path(path) escaped_embeds[escaped_path] = {} end JSON.pretty_generate(escaped_embeds.to_json) end def escape_path(path) escaped_path = path.gsub("'", "/:|").gsub('"', "/:|") (escaped_path[0] == "/") ? escaped_path.slice(1..-1) : escaped_path end end end end