From 1b6b4f998a25f375f50cda545b3f39ad19b42cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?O=CC=81scar=20M=2E=20Lage?= Date: Tue, 9 Apr 2024 20:06:34 +0200 Subject: [PATCH] Initial commit --- README.md | 39 +++++++++ go.mod | 15 ++++ go.sum | 18 ++++ main.go | 207 ++++++++++++++++++++++++++++++++++++++++++++ obs2hugo.ini.sample | 6 ++ 5 files changed, 285 insertions(+) create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 obs2hugo.ini.sample diff --git a/README.md b/README.md new file mode 100644 index 0000000..097ffa5 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# obs2hugo + +This repository contains a Go program that allows converting and copying contents from a custom Obsidian directory to Hugo-compatible content. Obsidian is a note-taking app that utilizes Markdown as its file format. Hugo, on the other hand, is a static site generator that allows creating websites from Markdown content. + +## Key Features + +- **Content Conversion**: The program converts Markdown files from the Obsidian directory to Hugo-compatible Markdown files. +- **Image Copying**: It copies the images included in Markdown files to a specific directory within Hugo's directory structure. +- **Configuration File Generation**: It creates additional configuration files necessary for Hugo to correctly process the content. + +## Configure + +Configuration runs in `~/.config/obs2hugo/obs2hugo.ini` or `~/.config/obs2hugo.ini`, something like this: + +```ini +watcher_dir = /Users/johndoe/vaults/obsidian/posts-hugo +hugo_dir = /Users/johndoe/code/hugo/src/content/posts +``` + +## Usage + +1. Clone the repository to your local machine. +2. Run the program providing the path to the Obsidian directory and the destination directory for Hugo. +3. The program will convert Markdown files, copy images, and generate necessary configuration files. + +```sh +$ go run main.go +``` + +## Requirements + +- Go installed on your system. +- Have a Hugo project set up and ready to receive content. +- Have an Obsidian vault on your system. + +## Contribution + +Contributions are welcome! If you find bugs or wish to improve the program, feel free to open an issue or submit a pull request. + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8d435f3 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module obsidian-watcher + +go 1.21.4 + +require ( + github.com/fsnotify/fsnotify v1.7.0 + github.com/gosimple/slug v1.14.0 + gopkg.in/ini.v1 v1.67.0 +) + +require ( + github.com/gosimple/unidecode v1.0.1 // indirect + github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/sys v0.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..3706111 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gosimple/slug v1.14.0 h1:RtTL/71mJNDfpUbCOmnf/XFkzKRtD6wL6Uy+3akm4Es= +github.com/gosimple/slug v1.14.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d7c6c27 --- /dev/null +++ b/main.go @@ -0,0 +1,207 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "log" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/fsnotify/fsnotify" + "github.com/gosimple/slug" + "gopkg.in/ini.v1" +) + +func main() { + + // Read the config file + home, err := os.UserHomeDir() + if err != nil { + log.Fatalln(err) + } + config, err := ini.Load(home + "/.config/obs2hugo/obs2hugo.ini") + if err != nil { + config, err = ini.Load(home + "/.config/obs2hugo.ini") + if err != nil { + log.Fatalln(err) + } + } + // Watcher directory + directory := config.Section("").Key("watcher_dir").String() + + // Hugo content directory + destDir := config.Section("").Key("hugo_dir").String() + + // Crearing watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + fmt.Println("Error creating the watcher:", err) + return + } + defer watcher.Close() + + // Add directory to the watcher + err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println("Error accessing to the directory:", err) + return err + } + if info.IsDir() { + return watcher.Add(path) + } + return nil + }) + if err != nil { + fmt.Println("Error adding directory to the watcher:", err) + return + } + + // Watcher event loop + for { + select { + case event := <-watcher.Events: + switch { + case event.Op&fsnotify.Create == fsnotify.Create: + fmt.Println("New file:", event.Name) + // TBD + case event.Op&fsnotify.Write == fsnotify.Write: + fmt.Println("File modified:", event.Name) + processModifiedFile(event.Name, directory, destDir) + case event.Op&fsnotify.Remove == fsnotify.Remove: + fmt.Println("File deleted:", event.Name) + // TBD + case event.Op&fsnotify.Rename == fsnotify.Rename: + fmt.Println("File renamed:", event.Name) + // TBD + } + case err := <-watcher.Errors: + fmt.Println("Watcher error:", err) + } + } +} + +func processModifiedFile(filePath, directory, destDir string) { + // Slugified file name without extension + fileName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath)) + slug := slug.Make(fileName) + + // Creating destiny directory + newDir := filepath.Join(destDir, slug) + err := os.MkdirAll(newDir, os.ModePerm) + if err != nil { + fmt.Println("Error creating directory:", err) + return + } + + // Copy modified file to the new destiny + newFilePath := filepath.Join(newDir, "index.md") + err = copyFile(filePath, newFilePath) + if err != nil { + fmt.Println("Error copying file:", err) + return + } + + // Move images to the gallery directory + err = moveImagesToGallery(filePath, newDir, directory) + if err != nil { + fmt.Println("Error moving images to the gallery:", err) + return + } +} + +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + newFile, err := os.Create(dst) + if err != nil { + return err + } + defer newFile.Close() + + _, err = io.Copy(newFile, sourceFile) + if err != nil { + return err + } + return nil +} + +func moveImagesToGallery(filePath, destDir, directory string) error { + // Routes + destFilePath := filepath.Join(destDir, "index.md") + slugDir := slug.Make(strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))) + galleryDir := filepath.Join(destDir, "gallery") + + // Read file content + content, err := os.ReadFile(filePath) + if err != nil { + return err + } + + // Regexp to find the markdown images + re := regexp.MustCompile(`!\[\[([^]]+)\]\]`) + matches := re.FindAllStringSubmatch(string(content), -1) + + // Create gallery dir if it doesn't exist + if _, err := os.Stat(galleryDir); os.IsNotExist(err) { + err := os.Mkdir(galleryDir, os.ModePerm) + if err != nil { + return err + } + } + + // Move every image found + for _, match := range matches { + imagePath := match[1] + imageName := filepath.Base(imagePath) + ext := filepath.Ext(imageName) + slugName := slug.Make(strings.TrimSuffix(imageName, ext)) + imageSrc := filepath.Join(directory, imagePath) + imageDest := filepath.Join(galleryDir, slugName+ext) + // Open source file + srcFile, err := os.Open(imageSrc) + if err != nil { + fmt.Printf("Error opening source file %s: %s\n", imageSrc, err) + continue + } + defer srcFile.Close() + + // Create destination file + destFile, err := os.Create(imageDest) + if err != nil { + fmt.Printf("Error creating destination file %s: %s\n", imageDest, err) + continue + } + defer destFile.Close() + + // Copy source to destination + _, err = io.Copy(destFile, srcFile) + if err != nil { + fmt.Printf("Error copying file %s to %s: %s\n", imageSrc, imageDest, err) + continue + } + + // Modify link text + newImageMarkdown := fmt.Sprintf("![%s](%s/gallery/%s)", strings.TrimSuffix(slugName, ext), slugDir, slugName+ext) + content = bytes.Replace(content, []byte(match[0]), []byte(newImageMarkdown), -1) + + fmt.Printf("Image %s was copied to %s\n", imageSrc, imageDest) + } + + // Replace [gallery] pattern + content = bytes.Replace(content, []byte("[gallery]"), []byte("{{< gallery match=\"gallery/*\" sortOrder=\"asc\" rowHeight=\"150\" margins=\"5\" thumbnailResizeOptions=\"600x600 q90 Lanczos\" previewType=\"blur\" embedPreview=\"true\" >}}"), -1) + + // Write markdown file new content + err = os.WriteFile(destFilePath, content, os.ModePerm) + if err != nil { + fmt.Printf("Error writing file content to the file: %s: %s\n", filePath, err) + } + + return nil +} diff --git a/obs2hugo.ini.sample b/obs2hugo.ini.sample new file mode 100644 index 0000000..ae56569 --- /dev/null +++ b/obs2hugo.ini.sample @@ -0,0 +1,6 @@ +; Remember: move this file to the proper directory: +; ~/.config/obs2hugo/obs2hugo.ini or +; ~/.config/obs2hugo.ini + +watcher_dir = /Users/johndoe/vaults/obsidian/posts-hugo +hugo_dir = /Users/johndoe/code/hugo/src/content/posts