Initial commit

main
Óscar M. Lage 2024-04-09 20:06:34 +02:00
commit 1b6b4f998a
5 changed files with 285 additions and 0 deletions

39
README.md Normal file
View File

@ -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.

15
go.mod Normal file
View File

@ -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
)

18
go.sum Normal file
View File

@ -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=

207
main.go Normal file
View File

@ -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
}

6
obs2hugo.ini.sample Normal file
View File

@ -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