Initial commit
This commit is contained in:
commit
1b6b4f998a
39
README.md
Normal file
39
README.md
Normal 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
15
go.mod
Normal 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
18
go.sum
Normal 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
207
main.go
Normal 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
6
obs2hugo.ini.sample
Normal 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
|
Loading…
Reference in New Issue
Block a user