diff --git a/.gitignore b/.gitignore index b602352..3a49983 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ -test* -output.* -.env* -config.json -_debug* \ No newline at end of file +config.toml +*_markdown/ +.env \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 1b85eea..be362ec 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "go", "request": "launch", "mode": "auto", - "program": "${workspaceFolder}/cobra", + "program": "${workspaceFolder}", "args": ["publish", "--help"], "console": "integratedTerminal" }, @@ -14,17 +14,17 @@ "name": "Publish from blogger", "type": "go", "request": "launch", - "program": "${workspaceFolder}/cobra", + "program": "${workspaceFolder}", // https://www.blogger.com/edit-profile.g "args": ["publish", "blogger", "${input:PostURL}", "${input:Destination}"], - // "envFile": "${workspaceFolder}/cobra/.env", + // "envFile": "${workspaceFolder}/.env", "console": "integratedTerminal" }, { "name": "Publish from Markdown", "type": "go", "request": "launch", - "program": "${workspaceFolder}/cobra", + "program": "${workspaceFolder}", "args": ["publish", "markdown", "${input:MarkdownPath}", "${input:Destination}"], "console": "integratedTerminal" } diff --git a/README.md b/README.md index 9bc9df6..56c9f12 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # cross-blogger -Cross-service (and cross-platform) blog posting utility +Soon-to-be headless CMS for static site generators powered by Google's Blogger. ### Installation #### Compiled Binaries @@ -11,25 +11,28 @@ Using `go install`, you can compile and add the program to the PATH. Either run `go install github.com/slashtechno/cross-blogger@latest`, follow the same process as compiling the program locally, but replace `go build` with `go install`. ### Usage -To use this program, just run the executable in the terminal. +Sources and destinations should first be configured in the `config.yaml` file. +For Google OAuth, the `--client-id` and `--client-secret` flags are required but can be set as environment variables (`CROSS_BLOGGER_GOOGLE_CLIENT_ID`/`CROSS_BLOGGER_GOOGLE_CLIENT_SECRET`). However these can also be set in the `config.yaml` file, passed as environment variables, or put in a `.env` file. When a refresh token is not provided, the program will commence the OAuth flow. This will write the refresh token, along with any other configuration, to the `config.yaml` file. If you prefer to use other methods to pass the credentials, you can remove the lines and use the other methods. #### Help Output -From `cross-blogger --help` +From `cross-blogger publish --help` ```text -Usage: cross-blogger.exe --client-id CLIENT-ID --client-secret CLIENT-SECRET [--refresh-token REFRESH-TOKEN] [--log-level LOG-LEVEL] [--log-color] [] - -Options: - --client-id CLIENT-ID - Google OAuth client ID [env: CLIENT_ID] - --client-secret CLIENT-SECRET - Google OAuth client secret [env: CLIENT_SECRET] - --refresh-token REFRESH-TOKEN - Google OAuth refresh token [env: REFRESH_TOKEN] - --log-level LOG-LEVEL - "debug", "info", "warning", "error", or "fatal" [default: info, env: LOG_LEVEL] - --log-color Force colored logs [default: false, env: LOG_COLOR] - --help, -h display this help and exit +Publish to a destination from a source. + Specify the source with the first positional argument. + The second positional argument is the specifier, such as a Blogger post URL or a file path. + All arguments after the first are treated as destinations. + Destinations should be the name of the destinations specified in the config file -Commands: - google-oauth Store Google OAuth refresh token - publish Publish to a destination +Usage: + cross-blogger publish [flags] + +Flags: + -r, --dry-run Don't actually publish + --google-client-id string Google OAuth client ID + --google-client-secret string Google OAuth client secret + --google-refresh-token string Google OAuth refresh token + -h, --help help for publish + -t, --title string Specify custom title instead of using the default + +Global Flags: + --config string config file path (default "config.toml") ``` diff --git a/args.go b/args.go deleted file mode 100644 index fb3ad28..0000000 --- a/args.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -type BloggerCmd struct { - BlogAddress string `arg:"positional, required" help:"Blog address to get content from"` - PostAddress string `arg:"positional, required" help:"Post slug to get content from"` -} - -type FileCmd struct { - Filepath string `arg:"positional, required" help:"Filepath to get content from"` -} - -type PublishCmd struct { - File *FileCmd `arg:"subcommand:file" help:"Publish from a file"` - Blogger *BloggerCmd `arg:"subcommand:blogger" help:"Publish from Blogger"` - // Perhaps instead of needing both a key and a value for destinations, parse a single value - // For example, check if it's a file, and if so, check the file ending to determine the type - // Maybe check if it contains blogger.com - // Of course, an override would be nice - Destinations map[string]string `arg:"--destinations, required" help:"Destination(s) to publish to\nAvailable destinations: blogger, markdown, html\nMake sure to specify with ="` - Title string `arg:"-t,--title" help:"Specify custom title instead of using the default"` - DryRun bool `arg:"-d,--dry-run" help:"Don't actually publish"` -} - -type GoogleOauthCmd struct { -} - -var Args struct { - // Subcommands - GoogleOauth *GoogleOauthCmd `arg:"subcommand:google-oauth" help:"Store Google OAuth refresh token"` - Publish *PublishCmd `arg:"subcommand:publish" help:"Publish to a destination"` - - // Google OAuth flags - ClientId string `arg:"--client-id, env:CLIENT_ID" help:"Google OAuth client ID"` - ClientSecret string `arg:"--I client-secret, env:CLIENT_SECRET" help:"Google OAuth client secret"` - RefreshToken string `arg:"--refresh-token, env:REFRESH_TOKEN" help:"Google OAuth refresh token" default:""` - - // Misc flags - LogLevel string `arg:"--log-level, env:LOG_LEVEL" help:"\"debug\", \"info\", \"warning\", \"error\", or \"fatal\"" default:"info"` - LogColor bool `arg:"--log-color, env:LOG_COLOR" help:"Force colored logs" default:"false"` -} diff --git a/cobra/cmd/platforms.go b/cmd/platforms.go similarity index 99% rename from cobra/cmd/platforms.go rename to cmd/platforms.go index 7230b8d..ef9379e 100644 --- a/cobra/cmd/platforms.go +++ b/cmd/platforms.go @@ -15,7 +15,7 @@ import ( md "github.com/JohannesKaufmann/html-to-markdown" "github.com/charmbracelet/log" "github.com/go-resty/resty/v2" - "github.com/slashtechno/cross-blogger/cobra/pkg/oauth" + "github.com/slashtechno/cross-blogger/pkg/oauth" "github.com/spf13/afero" "go.abhg.dev/goldmark/frontmatter" ) diff --git a/cobra/cmd/publish.go b/cmd/publish.go similarity index 100% rename from cobra/cmd/publish.go rename to cmd/publish.go diff --git a/cobra/cmd/root.go b/cmd/root.go similarity index 100% rename from cobra/cmd/root.go rename to cmd/root.go diff --git a/cobra/.gitignore b/cobra/.gitignore deleted file mode 100644 index 6b97628..0000000 --- a/cobra/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -config.toml -*_markdown/ \ No newline at end of file diff --git a/cobra/config.toml.example b/cobra/config.toml.example deleted file mode 100644 index 87244fc..0000000 --- a/cobra/config.toml.example +++ /dev/null @@ -1,5 +0,0 @@ -[[destinations]] -name = 'blogger' -blog_url = 'https://example.com' -type = 'blogger' - diff --git a/cobra/main.go b/cobra/main.go deleted file mode 100644 index b5db32d..0000000 --- a/cobra/main.go +++ /dev/null @@ -1,71 +0,0 @@ -/* -Copyright © 2024 NAME HERE -*/ -package main - -import ( - "io/fs" - - "github.com/charmbracelet/log" - "github.com/slashtechno/cross-blogger/cobra/cmd" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/subosito/gotenv" -) - -var cfgFile string - -func init() { - cobra.OnInitialize(initConfig) - cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.toml", "config file path") -} - -func initConfig() { - // Load a .env file if it exists - gotenv.Load() - // Tell Viper to use the prefix "CROSS_BLOGGER" for environment variables - viper.SetEnvPrefix("CROSS_BLOGGER") - // log.Debug(cfgFile) - if cfgFile != "" { - // Use config file from the flag. - viper.SetConfigFile(cfgFile) - } else { - // Use config.yaml in the current working directory. - viper.SetConfigFile("./config.toml") - } - - if err := viper.ReadInConfig(); err == nil { - log.Debug("", "config file:", viper.ConfigFileUsed()) - } else { - // If the config file is not found, create a file, write the default values and exit - // Since viper.ConfigFileNotFoundError doesn't always work, also use fs.PathError - if _, ok := err.(*fs.PathError); ok { - log.Debug("Config file not found, creating a new one") - viper.SetDefault("destinations", []map[string]interface{}{ - { - "name": "blogger", - "type": "blogger", - "blog_url": "https://example.com", - "overwrite": false, - }, - { - "name": "markdown1", - "type": "markdown", - "content_dir": "content", - "overwrite": false, - }, - }) - log.Fatal("Failed to read config file. Created a config file with default values. Please edit the file and run the command again.", "path", cfgFile) - if err := viper.WriteConfigAs(cfgFile); err != nil { - log.Fatal("Failed to write config file:", err) - } - } else { - log.Fatal("Failed to read config file:", err) - } - } -} - -func main() { - log.SetLevel(log.DebugLevel) - cmd.Execute() -} diff --git a/config.toml.example b/config.toml.example new file mode 100644 index 0000000..de67c60 --- /dev/null +++ b/config.toml.example @@ -0,0 +1,26 @@ +# type is a required field that specifies the type of the source or destination. +# name is the name of the source or destination. It is used to refer to the source or destination when running the command. +# overwrite is a boolean field that specifies whether to overwrite the file/post if it already exists. This is done by removing old files/posts that have the same title. +# blog_url is the URL of the blog +# content_dir is the directory where the markdown files are located +[[destinations]] +blog_url = 'https://example.com' +name = 'blog' +overwrite = false +type = 'blogger' + +[[destinations]] +content_dir = 'content' +name = 'otherblog' +overwrite = false +type = 'markdown' + +[[sources]] +blog_url = 'https://example.com' +name = 'someblog' +type = 'blogger' + +[[sources]] +content_dir = 'content' +name = 'aBlogInMarkdown' +type = 'markdown' diff --git a/main.go b/main.go index 9a8adc7..323ff3f 100644 --- a/main.go +++ b/main.go @@ -1,425 +1,83 @@ -// When running, use `go run .` package main import ( - "bufio" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" + "io/fs" - htmltomd "github.com/JohannesKaufmann/html-to-markdown" - "github.com/alexflint/go-arg" - mdlib "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/parser" - "github.com/imdario/mergo" - "github.com/joho/godotenv" - "github.com/sirupsen/logrus" - "github.com/skratchdot/open-golang/open" - "github.com/tidwall/gjson" + "github.com/charmbracelet/log" + "github.com/slashtechno/cross-blogger/cmd" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/subosito/gotenv" ) -func main() { - godotenv.Load(".env") - arg.MustParse(&Args) - - logrus.SetOutput(os.Stdout) - logrus.SetFormatter(&logrus.TextFormatter{PadLevelText: true, DisableQuote: true, ForceColors: Args.LogColor, DisableColors: !Args.LogColor}) - if Args.LogLevel == "debug" { - logrus.SetLevel(logrus.DebugLevel) - // Enable line numbers in debug logs - Doesn't help too much since a fatal error still needs to be debugged - logrus.SetReportCaller(true) - } else if Args.LogLevel == "info" { - logrus.SetLevel(logrus.InfoLevel) - } else if Args.LogLevel == "warning" { - logrus.SetLevel(logrus.WarnLevel) - } else if Args.LogLevel == "error" { - logrus.SetLevel(logrus.ErrorLevel) - } else if Args.LogLevel == "fatal" { - logrus.SetLevel(logrus.FatalLevel) - } else { - logrus.SetLevel(logrus.InfoLevel) - } +var cfgFile string - switch { - case Args.GoogleOauth != nil: - _, err := getAccessToken() - if err != nil { - logrus.Fatal(err) - } - case Args.Publish != nil: - var ( - title string - html string - markdown string - err error - ) - switch { - case Args.Publish.Blogger != nil: - title, html, markdown, err = getBloggerPost(Args.Publish.Blogger.BlogAddress, Args.Publish.Blogger.PostAddress) - if err != nil { - logrus.Fatal(err) - } - case Args.Publish.File != nil: - title, html, markdown, err = getFilePost(Args.Publish.File.Filepath, Args.Publish.Title) - if err != nil { - logrus.Fatal(err) - } - default: - // Could add an interactive mode here for user-friendliness - logrus.Fatal("No subcommand specified") - } - if Args.Publish.DryRun { - logrus.Debugf("Title: %s | HTML: %s | Markdown: %s", title, html, markdown) - } else { - err = publishPost(title, html, markdown, Args.Publish.Destinations) - } - if err != nil { - logrus.Fatal(err) - } - } +func init() { + cobra.OnInitialize(initConfig) + cmd.RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "config.toml", "config file path") } -func publishPost(title string, html string, markdown string, destinations map[string]string) error { - for destination, destinationSpecifier := range destinations { - switch destination { - case "blogger": - // Get the blog ID - blogId, err := getBlogId(destinationSpecifier) - if err != nil { - return err - } - // Publish to Blogger - logrus.Info("Publishing to Blogger") - url := "https://www.googleapis.com/blogger/v3/blogs/" + blogId + "/posts/" - payloadMap := map[string]interface{}{ - "kind": "blogger#post", - "blog": map[string]string{ - "id": blogId, - }, - "title": title, - "content": html, - } - accessToken, err := getAccessToken() - if err != nil { - return err - } - _, err = request(url, "POST", accessToken, payloadMap) - if err != nil { - return err - } - case "markdown": - logrus.Info("Publishing to Markdown") - cleanPathToFile := filepath.Clean(destinationSpecifier) - // Open the file in write-only mode (600) with append and create - file, err := os.OpenFile(cleanPathToFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) - if err != nil { - return err - } - // Write markdown to file - _, err = file.WriteString(markdown) - if err != nil { - return err - } - // Close the file - err = file.Close() - if err != nil { - return err - } - - } - } - return nil -} -func storeRefreshToken() (string, error) { // Rename to getRefreshToken(), perhaps? - err := checkNeededFlags(map[string]string{"clientId": Args.ClientId, "clientSecret": Args.ClientSecret}) - if err != nil { - return "", err - } - // Get the authorization code from the user - url := "https://accounts.google.com/o/oauth2/v2/auth?client_id=" + Args.ClientId + "&redirect_uri=https%3A%2F%2Foauthcodeviewer.netlify.app&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fblogger&response_type=code&access_type=offline&prompt=consent" - // Open the URL in the default browser - err = open.Run(url) - fmt.Println("If the link didn't open, please manually go to the following link in your browser:") - // Print the URL - fmt.Printf("\n%v\n\n", url) - if err != nil { - return "", err - } - fmt.Println("Input the authorization code below") - authorizationCode, err := singleLineInput() - if err != nil { - return "", err - } - - // Get refresh token using the authorization code given by the user - url = "https://oauth2.googleapis.com/token?client_id=" + Args.ClientId + "&client_secret=" + Args.ClientSecret + "&code=" + authorizationCode + "&redirect_uri=https%3A%2F%2Foauthcodeviewer.netlify.app&grant_type=authorization_code" - // Send a POST request to the URL with no authorization headers - resultBody, err := request(url, "POST", "", nil) - if err != nil { - return "", err - } - googleRefreshToken := gjson.Get(resultBody, "refresh_token").String() - logrus.Debugf("Refresh token: %s", googleRefreshToken) - // Merge the new environment variable with the existing environment variables using Mergo - env := map[string]string{"REFRESH_TOKEN": googleRefreshToken} - oldEnv, err := godotenv.Read() - if err != nil { - return "", err - } - err = mergo.Merge(&env, oldEnv) - // for key, value := range oldEnv { - // env[key] = value - // } - - if err != nil { - return "", err - } - - logrus.Debugf("New environment variables: %v | Old enviroment variables %v", env, oldEnv) - // May want to use filepath.Join() here - err = godotenv.Write(env, ".env") - if err != nil { - return "", err - } - return googleRefreshToken, nil -} - -func getAccessToken() (string, error) { - err := checkNeededFlags(map[string]string{"clientId": Args.ClientId, "clientSecret": Args.ClientSecret}) - if err != nil { - return "", err - } - var googleRefreshToken string - // Check if there is a refresh token present - if Args.RefreshToken == "" { - logrus.Print("No refresh token found. Please input the following information to get a refresh token.\n") - googleRefreshToken, err = storeRefreshToken() - if err != nil { - return "", err - } +func initConfig() { + // Load a .env file if it exists + gotenv.Load() + // Tell Viper to use the prefix "CROSS_BLOGGER" for environment variables + viper.SetEnvPrefix("CROSS_BLOGGER") + // log.Debug(cfgFile) + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) } else { - googleRefreshToken = Args.RefreshToken - - } - - // Get access token using the refresh token - url := "https://oauth2.googleapis.com/token?client_id=" + Args.ClientId + "&client_secret=" + Args.ClientSecret + "&refresh_token=" + googleRefreshToken + "&redirect_uri=https%3A%2F%2Foauthcodeviewer.netlify.app&grant_type=refresh_token" - // Send a POST request to the URL with no authorization headers - resultBody, err := request(url, "POST", "", nil) - if err != nil { - return "", err - } - // Get the authorization token - accessToken := gjson.Get(resultBody, "access_token").String() - if accessToken == "" { - return "", errors.New("no access token found") - } else { - logrus.Debugf("Access token: %s", accessToken) - - } - return accessToken, nil -} - -func singleLineInput() (string, error) { - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - return "", err + // Use config.yaml in the current working directory. + viper.SetConfigFile("./config.toml") } - input = strings.TrimSpace(input) - // fmt.Print("\n") - return input, nil -} -func request(url string, requestType string, bearerAuth string, payloadMap map[string]interface{}) (string, error) { - // Send a request to the URL, with the URL which was passed to the function - var req *http.Request - var err error - // If payloadMap is nil, don't send a payload - if payloadMap == nil { - req, err = http.NewRequest(requestType, url, nil) - if err != nil { - return "", err - } + if err := viper.ReadInConfig(); err == nil { + log.Debug("", "config file:", viper.ConfigFileUsed()) } else { - // logrus.Debugf("Payload map: %v", payloadMap) - payloadBytes, err := json.Marshal(payloadMap) - if err != nil { - return "", err - } - payload := strings.NewReader(string(payloadBytes)) - req, err = http.NewRequest(requestType, url, payload) - if err != nil { - return "", err - } - } - - // If the bearerAuth parameter is true, set the Authorization header with an access token - if bearerAuth != "" { - req.Header.Add("Authorization", "Bearer "+bearerAuth) - } - // Make the actual request - res, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - // Convert the result body to a string and then return it - resultBodyBytes, err := io.ReadAll(res.Body) - if err != nil { - return "", err - } - err = res.Body.Close() - if err != nil { - return "", err - } - // Debug the status code - // logrus.Debugf("Sending %s request to %s with payload %v, bearer authorization %s. Got status code %d", requestType, url, payloadMap, bearerAuth, res.StatusCode) - return string(resultBodyBytes), nil -} - -func getBlogId(blogAddress string) (string, error) { - // Send a GET request to the URL with bearer authorization - accessToken, err := getAccessToken() - logrus.Debugf("Blog address: %s", blogAddress) - url := "https://www.googleapis.com/blogger/v3/blogs/byurl?url=https%3A%2F%2F" + blogAddress - if err != nil { - return "", err - } - resultBody, err := request(url, "GET", accessToken, nil) - if err != nil { - return "", err - } - // logrus.Debugf("Result body: %s", resultBody) - // Get the blog ID - id := gjson.Get(resultBody, "id").String() - if id == "" { - return "", errors.New("no blog ID found") - } - return id, nil -} - -func getBloggerPost(blogAddress string, postAddress string) (string, string, string, error) { - path := strings.Replace(postAddress, blogAddress, "", 1) - blogID, err := getBlogId(blogAddress) - logrus.Debugf("Blog ID: %s | Path: %s", blogID, path) - if err != nil { - return "", "", "", err - } - accessToken, err := getAccessToken() - if err != nil { - return "", "", "", err - } - // https://www.googleapis.com/blogger/v3/blogs/[BLOG_ID]/posts/bypath?path=/{YEAR}/{MONTH}/{POST}.html - url := "https://www.googleapis.com/blogger/v3/blogs/" + blogID + "/posts/bypath?path=" + path - resultBody, err := request(url, "GET", accessToken, nil) - if err != nil { - return "", "", "", err - } - html := gjson.Get(resultBody, "content").String() - title := gjson.Get(resultBody, "title").String() - markdown, err := htmltomd.NewConverter("", true, nil).ConvertString(html) - if title == "" && html == "" && markdown == "" { - logrus.Debug(url) - return "", "", "", errors.New("no post found") - } - if err != nil { - return "", "", "", err - } - return title, html, markdown, nil -} - -func getFilePost(pathToFile string, defaultTitle string) (string, string, string, error) { - // If the file path is empty, ask for it (might be a good idea to remove this) - if pathToFile == "" { - fmt.Println("Enter the path to the file") - var err error - pathToFile, err = singleLineInput() - if err != nil { - return "", "", "", err - } - } - - cleanPathToFile := filepath.Clean(pathToFile) - - // Check if the file exists - _, err := os.Stat(cleanPathToFile) - if errors.Is(err, os.ErrNotExist) { - return "", "", "", errors.New("file does not exist") - } - - // Read the file - fileBytes, err := os.ReadFile(cleanPathToFile) - if err != nil { - return "", "", "", err - } - fileContent := string(fileBytes) - logrus.Debug("The file was read successfully") - var ( - title string - html string - markdown string - ) + // If the config file is not found, create a file, write the default values and exit + // Since viper.ConfigFileNotFoundError doesn't always work, also use fs.PathError + if _, ok := err.(*fs.PathError); ok { + log.Debug("Config file not found, creating a new one") + // Destinations + viper.SetDefault("destinations", []map[string]interface{}{ + { + "name": "blog", + "type": "blogger", + "blog_url": "https://example.com", + "overwrite": false, + }, + { + "name": "otherblog", + "type": "markdown", + "content_dir": "content", + "overwrite": false, + }, + }) + // Sources + viper.SetDefault("sources", []map[string]interface{}{ + { + "name": "someblog", + "type": "blogger", + "blog_url": "https://example.com", + }, + { + "name": "aBlogInMarkdown", + "type": "markdown", + "content_dir": "content", + }, + }) - // Check file extension - fileExtension := filepath.Ext(pathToFile) - switch fileExtension { - case ".html", ".htm": - // Set HTML to the file content - html = fileContent - // Convert to Markdown - markdown, err = htmltomd.NewConverter("", true, nil).ConvertString(fileContent) - if err != nil { - return "", "", "", err + if err := viper.WriteConfigAs(cfgFile); err != nil { + log.Fatal("Failed to write config file:", err) + } + log.Fatal("Failed to read config file. Created a config file with default values. Please edit the file and run the command again.", "path", cfgFile) + } else { + log.Fatal("Failed to read config file:", err) } - - case ".md", ".markdown": - // Set Markdown to the file content - markdown = fileContent - // Convert to HTML - extensions := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(extensions) - html = string(mdlib.ToHTML([]byte(fileContent), parser, nil)) - - case ".txt": - // Not sure if plain text should be supported but it can easily be removed later - - // Set Markdown to the file content - markdown = fileContent - // Convert to HTML - extensions := parser.CommonExtensions | parser.AutoHeadingIDs - parser := parser.NewWithExtensions(extensions) - html = string(mdlib.ToHTML([]byte(fileContent), parser, nil)) - default: - return "", "", "", errors.New("file extension not supported") - } - if defaultTitle != "" { - title = defaultTitle - } else { - // Get the file name without the extension - fileName := filepath.Base(pathToFile) - fileNameWithoutExtension := strings.TrimSuffix(fileName, filepath.Ext(fileName)) - // Replace underscores with spaces (Might be a good idea to make this optional) - title = strings.ReplaceAll(fileNameWithoutExtension, "_", " ") } - return title, html, markdown, nil } -func checkNeededFlags(flags map[string]string) error { - message := "The following must be set" - for name, value := range flags { - if value == "" { - message += "\n- " + name - } - if message != "The following must be set" { - return errors.New(message) - } - } - return nil +func main() { + log.SetLevel(log.DebugLevel) + cmd.Execute() } - -// Check if the file exists -> Check file extension -> Convert to the other formats -> Check if user-defined title is set -> If user-defined title is not set, use the file name as the title -> Return the title, HTML, and Markdown and hopefully no errors. diff --git a/cobra/pkg/oauth/google.go b/pkg/oauth/google.go similarity index 100% rename from cobra/pkg/oauth/google.go rename to pkg/oauth/google.go