summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'cmd')
-rw-r--r--cmd/cat.go94
-rw-r--r--cmd/export.go123
-rw-r--r--cmd/hash.go98
-rw-r--r--cmd/import.go203
-rw-r--r--cmd/info.go94
-rw-r--r--cmd/ls.go168
-rw-r--r--cmd/root.go36
-rw-r--r--cmd/tag/add.go88
-rw-r--r--cmd/tag/remove.go87
-rw-r--r--cmd/tag/show.go82
-rw-r--r--cmd/tag/tag.go63
11 files changed, 1136 insertions, 0 deletions
diff --git a/cmd/cat.go b/cmd/cat.go
new file mode 100644
index 0000000..688f10f
--- /dev/null
+++ b/cmd/cat.go
@@ -0,0 +1,94 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "encoding/json"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/spf13/cobra"
+)
+
+// catCmd represents the cat command
+var catCmd = &cobra.Command{
+ Use: "cat",
+ Short: "cat prints highlights of pdf files [unix][json]",
+ Long: `
+ ghligh cat file1.pdf file2.pdf ... [--json] [-i]
+
+ will show every highlights inside pdf files specified
+ if --json is set the output will be in json format
+
+ if -i is set the json output will be indented
+`,
+ Run: func(cmd *cobra.Command, args []string) {
+
+ useJSON, err := cmd.Flags().GetBool("json")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ indent, err := cmd.Flags().GetBool("indent")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ jsonCat := make(map[string][]document.HighlightedText)
+
+ // for every arg
+ for _, arg := range args {
+ doc, err := document.Open(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ continue
+ }
+
+ highlights := doc.Cat()
+ if !useJSON {
+ for _, highlight := range highlights {
+ if highlight.Contents != "" {
+ fmt.Printf("%s {{{%s}}}", highlight.Text, highlight.Contents)
+ } else {
+ fmt.Printf("%s", highlight.Text)
+ }
+ }
+ } else {
+ jsonCat[doc.Path] = highlights
+ }
+
+ doc.Close()
+ }
+
+ var jsonBytes []byte
+ if indent {
+ jsonBytes, err = json.MarshalIndent(jsonCat, "", " ")
+ } else {
+ jsonBytes, err = json.Marshal(jsonCat)
+ }
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(string(jsonBytes))
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(catCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // catCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ catCmd.Flags().BoolP("json", "j", false, "print highlights as json")
+ catCmd.Flags().BoolP("indent", "i", false, "print highlights as json")
+}
diff --git a/cmd/export.go b/cmd/export.go
new file mode 100644
index 0000000..aaf822c
--- /dev/null
+++ b/cmd/export.go
@@ -0,0 +1,123 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/spf13/cobra"
+)
+
+var outputFiles []string
+
+func writeJSONToFile(jsonBytes []byte, path string) error {
+ file, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ // IF FILE EXISTS ASK
+ _, err = file.Write(jsonBytes)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// exportCmd represents the export command
+var exportCmd = &cobra.Command{
+ Use: "export",
+ Short: "export pdf highlights into json",
+ Long: `
+ ghligh export foo.pdf bar.pdf ... [--to fnord.json] [-1] [-i]
+
+ will create one or more json file (specified with --to) or dump it
+ to stdout (-1)
+
+ -i will indent the json output
+`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+
+ indent, err := cmd.Flags().GetBool("indent")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ stdout, err := cmd.Flags().GetBool("stdout")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ if !stdout && len(outputFiles) == 0 {
+ fmt.Fprintf(os.Stderr, "nowhere to put output I am not doing anything\n")
+ return
+ }
+
+ var exportedDocs []document.GhlighDoc
+ for _, file := range args {
+ doc, err := document.Open(file)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error loading %s: %v", file, err)
+ continue
+ }
+
+ doc.AnnotsBuffer = doc.GetAnnotsBuffer()
+ doc.HashBuffer = doc.HashDoc()
+ exportedDocs = append(exportedDocs, *doc)
+ }
+
+ var jsonBytes []byte
+ if indent {
+ jsonBytes, err = json.MarshalIndent(exportedDocs, "", " ")
+ } else {
+ jsonBytes, err = json.Marshal(exportedDocs)
+ }
+ if err != nil {
+ panic(err)
+ }
+
+ for _, file := range outputFiles {
+ err := writeJSONToFile(jsonBytes, file)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ }
+ }
+
+ if stdout {
+ fmt.Printf("%s\n", string(jsonBytes))
+ }
+
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(exportCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // exportCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // exportCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ // TODO flag toFiles
+ exportCmd.Flags().BoolP("indent", "i", false, "indent the json data")
+ exportCmd.Flags().BoolP("stdout", "1", false, "dump to stdout")
+
+ exportCmd.Flags().StringArrayVarP(&outputFiles, "to", "t", []string{}, "files to save exported annots")
+}
diff --git a/cmd/hash.go b/cmd/hash.go
new file mode 100644
index 0000000..5185736
--- /dev/null
+++ b/cmd/hash.go
@@ -0,0 +1,98 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "sync"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/spf13/cobra"
+)
+
+// hashCmd represents the hash command
+var hashCmd = &cobra.Command{
+ Use: "hash",
+ Short: "display the ghligh hash used to identify a documet [json]",
+ Long: `the ghligh hash is used to identify documents with different filenames / annotations and it is calculated using the text of some pages.
+
+ ghligh hash file1.json file2.json [-i]
+
+ -i will indent the json output
+ `,
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+ indent, err := cmd.Flags().GetBool("indent")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ // var hashes []document.GhlighDoc
+ hashChan := make(chan document.GhlighDoc)
+ var wg sync.WaitGroup
+ wg.Add(len(args))
+
+ for _, arg := range args {
+ go func(arg string) {
+ defer wg.Done()
+ doc, err := document.Open(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error opening %s: %v\n", arg, err)
+ return
+ }
+
+ // A little hacky, set hash after closing the document
+ //doc.Close()
+ doc.HashBuffer = doc.HashDoc()
+
+ hashChan <- *doc
+ }(arg)
+ }
+
+ go func() {
+ wg.Wait()
+ close(hashChan)
+ }()
+
+ var hashes []document.GhlighDoc
+ for doc := range hashChan {
+ hashes = append(hashes, doc)
+ doc.Close()
+ }
+
+ var jsonBytes []byte
+ if indent {
+ jsonBytes, err = json.MarshalIndent(hashes, "", " ")
+ } else {
+ jsonBytes, err = json.Marshal(hashes)
+ }
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(string(jsonBytes))
+
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(hashCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // hashCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // hashCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ hashCmd.Flags().BoolP("indent", "i", false, "indent the json data")
+}
diff --git a/cmd/import.go b/cmd/import.go
new file mode 100644
index 0000000..2bcb1d1
--- /dev/null
+++ b/cmd/import.go
@@ -0,0 +1,203 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "sync"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/spf13/cobra"
+
+ "crypto/sha256"
+)
+
+var inputFiles []string
+
+type importedAnnots struct {
+ // FIXME just internatl map[string]struct where
+ // struct contains map[string]bool and document.AnnotsMap
+ internal map[string]document.AnnotsMap
+ annotsHashes map[string]map[string]bool
+ mutex sync.Mutex
+}
+
+func (ia *importedAnnots) get(hash string) document.AnnotsMap {
+ return ia.internal[hash]
+}
+
+func (ia *importedAnnots) init(hash string) {
+ ia.mutex.Lock()
+ defer ia.mutex.Unlock()
+ if ia.internal[hash] == nil {
+ ia.internal[hash] = make(document.AnnotsMap)
+ }
+ if ia.annotsHashes[hash] == nil {
+ ia.annotsHashes[hash] = make(map[string]bool)
+ }
+}
+
+func (ia *importedAnnots) check(docHash string, annotsHash string) bool {
+ ia.mutex.Lock()
+ defer ia.mutex.Unlock()
+ ok := ia.annotsHashes[docHash][annotsHash]
+ return ok
+}
+
+func (ia *importedAnnots) insert(hash string, am document.AnnotsMap) error {
+ amHash, err := hashAnnotsMap(am)
+ if err != nil {
+ return err
+ }
+
+ present := ia.check(hash, amHash)
+ if !present {
+ ia.mutex.Lock()
+ for key, value := range am {
+ ia.internal[hash][key] = append(ia.internal[hash][key], value...)
+ ia.annotsHashes[hash][amHash] = true
+ }
+ ia.mutex.Unlock()
+ }
+ return nil
+}
+
+func hashAnnotsMap(am document.AnnotsMap) (string, error) {
+ jsonBytes, err := json.Marshal(am)
+ if err != nil {
+ return "", err
+ }
+
+ h := sha256.Sum256(jsonBytes)
+ return fmt.Sprintf("%x", h), nil
+}
+
+func loadImportedAnnots(ia *importedAnnots, reader io.Reader) {
+ data, err := io.ReadAll(reader)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "could not read data: %v\n", err)
+ return
+ }
+
+ var importedDocs []document.GhlighDoc
+
+ err = json.Unmarshal(data, &importedDocs)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ return
+ }
+
+ for _, importedDoc := range importedDocs {
+ hash := importedDoc.HashBuffer
+ ia.init(hash)
+ ia.insert(hash, importedDoc.AnnotsBuffer)
+ }
+}
+
+// importCmd represents the import command
+var importCmd = &cobra.Command{
+ Use: "import",
+ Short: "import highlights from json file",
+ Long: `
+ ghligh import foo.pdf bar.pdf ... [--from fnord.json] [--from kadio.json] [-0] [--save=false]
+
+ will import into foo.pdf bar.pdf etc... the highlights from file specified
+ with the --from flag
+
+ if -0 is set ghligh will read json from stdin
+
+ --save=false will run without saving documents, it will just tells you how
+ many annotations from the json files specified will be imported
+`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+
+ stdin, err := cmd.Flags().GetBool("stdin")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ save, err := cmd.Flags().GetBool("save")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ if stdin == false && len(inputFiles) == 0 {
+ fmt.Fprintf(os.Stderr, "nowhere to put output I am not doing anything\n")
+ return
+ }
+
+ // Load Annot Maps
+ ia := importedAnnots{
+ internal: make(map[string]document.AnnotsMap),
+ annotsHashes: make(map[string]map[string]bool),
+ }
+
+ var wg sync.WaitGroup
+
+ wg.Add(len(inputFiles))
+ for _, file := range inputFiles {
+ //wg.Add(1)
+ go func(path string) {
+ defer wg.Done()
+
+ f, err := os.Open(path)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "could not open file %s: %v\n", path, err)
+ return
+ }
+ defer f.Close()
+
+ loadImportedAnnots(&ia, f)
+ }(file)
+ }
+
+ wg.Wait()
+
+ if stdin {
+ loadImportedAnnots(&ia, os.Stdin)
+ }
+
+ // load from inputFiles
+ for _, file := range args {
+ doc, err := document.Open(file)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error loading %s: %v", file, err)
+ continue
+ }
+
+ hash := doc.HashDoc()
+
+ num, err := doc.Import(ia.get(hash))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "could not import highlights into %s: %v\n", file, err)
+ } else {
+ fmt.Fprintf(os.Stderr, "imported %d annots into %s\n", num, file)
+ if save {
+ doc.Save()
+ }
+ }
+ doc.Close()
+
+ }
+
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(importCmd)
+
+ importCmd.Flags().BoolP("stdin", "0", false, "read json from stdin")
+ importCmd.Flags().BoolP("save", "", true, "save the file with new annotation importer")
+ importCmd.Flags().StringArrayVarP(&inputFiles, "from", "f", []string{}, "files to import annots from")
+}
diff --git a/cmd/info.go b/cmd/info.go
new file mode 100644
index 0000000..7a33f62
--- /dev/null
+++ b/cmd/info.go
@@ -0,0 +1,94 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+ "fmt"
+ "os"
+
+ "sync"
+
+ "encoding/json"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/scrotadamus/ghligh/go-poppler"
+ "github.com/spf13/cobra"
+)
+
+// infoCmd represents the info command
+var infoCmd = &cobra.Command{
+ Use: "info",
+ Short: "display info about pdf documents [json]",
+ Long: `
+ ghligh info file1.pdf file2.pdf [-i]
+
+ shows information about pdf (author, publisher, modification date, etc...)
+ -i will indent the json output
+ `,
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+ indent, err := cmd.Flags().GetBool("indent")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ infoChan := make(chan poppler.DocumentInfo)
+ var wg sync.WaitGroup
+ wg.Add(len(args))
+
+ for _, arg := range args {
+ go func(arg string) {
+ defer wg.Done()
+ doc, err := document.Open(arg)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error opening %s: %v\n", arg, err)
+ return
+ }
+ defer doc.Close()
+ infoChan <- doc.Info()
+ }(arg)
+ }
+
+ go func() {
+ wg.Wait()
+ close(infoChan)
+ }()
+
+ var infos []poppler.DocumentInfo
+ for info := range infoChan {
+ infos = append(infos, info)
+ }
+
+ var jsonBytes []byte
+ if indent {
+ jsonBytes, err = json.MarshalIndent(infos, "", " ")
+ } else {
+ jsonBytes, err = json.Marshal(infos)
+ }
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(string(jsonBytes))
+
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(infoCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // infoCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // infoCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ infoCmd.Flags().BoolP("indent", "i", false, "indent the json data")
+}
diff --git a/cmd/ls.go b/cmd/ls.go
new file mode 100644
index 0000000..bcdbe66
--- /dev/null
+++ b/cmd/ls.go
@@ -0,0 +1,168 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "sync"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/spf13/cobra"
+)
+
+var recursive bool
+
+type resolver struct {
+ paths []string
+ recurse bool
+ ctx context.Context
+ ch chan<- string
+ wg sync.WaitGroup
+}
+
+func (r *resolver) resolve() {
+ for _, path := range r.paths {
+ r.wg.Add(1)
+ go r.resolvePath(path)
+ }
+
+ go func() {
+ r.wg.Wait()
+ close(r.ch)
+ }()
+}
+
+func (r *resolver) resolvePath(path string) {
+ defer r.wg.Done()
+ if err := r.ctx.Err(); err != nil {
+ return
+ }
+
+ entries, err := os.ReadDir(path)
+ if err != nil {
+ str, err := filepath.Abs(path)
+ if err != nil {
+ return
+ }
+
+ r.ch <- str
+ return
+ }
+
+ for _, entry := range entries {
+ info, err := entry.Info()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error retrieving info for %s: %v\n", entry.Name(), err)
+ continue
+ }
+
+ fullPath := filepath.Join(path, entry.Name())
+
+ if info.IsDir() {
+ if r.recurse {
+ if err := r.ctx.Err(); err != nil {
+ return
+ }
+ r.wg.Add(1)
+ go r.resolvePath(fullPath)
+ }
+ } else if info.Mode().IsRegular() {
+ r.ch <- fullPath
+ }
+ }
+}
+
+func lsArgs(args []string) []string {
+ if len(args) == 0 {
+ cwd, err := os.Getwd()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ return nil
+ }
+ return []string{cwd}
+
+ }
+ return args
+}
+
+func checkFile(path string) bool {
+ doc, err := document.Open(path)
+ if err != nil {
+ return false
+ }
+ defer doc.Close()
+
+ return doc.HasHighlights()
+}
+
+// lsCmd represents the ls command
+var lsCmd = &cobra.Command{
+ Use: "ls",
+ Short: "show files with highlights or tagged with 'ls' [unix]",
+ Long: `
+ ghligh ls file1.pdf directory [-R] [-c]
+
+ will show every file inside directory that contains highlights or it is marked
+ ls with the ghligh tag add command
+
+ ghligh ls # show files in current dir
+ ghligh ls file1.pdf # if it outpus file1.pdf it means that file1.pdf contains highlights
+ ghligh ls -c file1.pdf # same as ghligh ls file1.pdf but exit status will not be if file1.pdf doesnt
+ contains highlights
+
+ ghligh ls -R # do it recursively, be careful with symlink dir cycles, as I am to lazy to
+ address that particular issue
+`,
+ Run: func(cmd *cobra.Command, args []string) {
+ files := lsArgs(args)
+ ch := make(chan string)
+ ctx := context.Background()
+
+ res := resolver{
+ paths: files,
+ recurse: recursive,
+ ctx: ctx,
+ ch: ch,
+ }
+
+ go res.resolve()
+
+ var wg sync.WaitGroup
+ var found bool
+ for file := range ch {
+ wg.Add(1)
+ go func(f string) {
+ defer wg.Done()
+ if checkFile(f) {
+ found = true
+ fmt.Printf("%s\n", f)
+ }
+ }(file)
+ }
+ wg.Wait()
+
+ check, err := cmd.Flags().GetBool("check")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+ if check && !found {
+ os.Exit(1)
+ }
+
+ },
+}
+
+func init() {
+ rootCmd.AddCommand(lsCmd)
+
+ lsCmd.Flags().BoolVarP(&recursive, "recursive", "R", false, "List recursively")
+ lsCmd.Flags().BoolP("check", "c", false, "exit status is 1 if no file its found")
+ // order pdf by time of something (modification / creation) ???
+ //lsCmd.Flags().BoolP("time", "t", false, "ls by time")
+}
diff --git a/cmd/root.go b/cmd/root.go
new file mode 100644
index 0000000..321253c
--- /dev/null
+++ b/cmd/root.go
@@ -0,0 +1,36 @@
+/*
+Copyright © 2025 Francesco Orlando scrotadamus@insiberia.net
+*/
+package cmd
+
+import (
+ "os"
+
+ "github.com/scrotadamus/ghligh/cmd/tag"
+ "github.com/spf13/cobra"
+)
+
+var rootCmd = &cobra.Command{
+ Use: "ghligh",
+ Short: "pdf highlights swiss knife",
+ Long: `ghligh can be used to manipulate pdf files in various ways.`,
+
+ Run: func(cmd *cobra.Command, args []string) {
+ cmd.Help()
+ return
+ },
+}
+
+// Execute adds all child commands to the root command and sets flags appropriately.
+// This is called by main.main(). It only needs to happen once to the rootCmd.
+func Execute() {
+ err := rootCmd.Execute()
+ if err != nil {
+ os.Exit(1)
+ }
+}
+
+func init() {
+ //rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ rootCmd.AddCommand(tag.TagCmd)
+}
diff --git a/cmd/tag/add.go b/cmd/tag/add.go
new file mode 100644
index 0000000..a5dc258
--- /dev/null
+++ b/cmd/tag/add.go
@@ -0,0 +1,88 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+*/
+package tag
+
+import (
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/scrotadamus/ghligh/document"
+ "github.com/spf13/cobra"
+)
+
+func readStdin() string {
+ data, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ panic(err)
+ }
+ return string(data)
+}
+
+// tagAddCmd represents the tag command
+var tagAddCmd = &cobra.Command{
+ Use: "add",
+ Short: "add a ghligh tag to a pdf file",
+ Long: `A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+
+ stdin, err := cmd.Flags().GetBool("stdin")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+ if stdin {
+ tags = append(tags, readStdin())
+ }
+ if len(tags) == 0 {
+ fmt.Fprintf(os.Stderr, "Either --tag or --stdin is required\n", err)
+ os.Exit(1)
+ }
+
+ for _, file := range args {
+ doc, err := document.Open(file)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ continue
+ }
+ for _, tag := range tags {
+ doc.Tag(tag)
+ fmt.Fprintf(os.Stderr, "added tag {{{%s}}} to %s\n", tag, file)
+ }
+ doc.Save()
+ doc.Close()
+ }
+ },
+}
+
+func init() {
+ TagCmd.AddCommand(tagAddCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // tagCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // tagCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ // TODO add to "add" command
+ tagAddCmd.Flags().StringArrayVarP(&tags, "tag", "t", []string{}, "Tag da associare ai file (può essere usato più volte)")
+ tagAddCmd.Flags().BoolP("stdin", "0", false, "read tag from stdin")
+
+ //if err := tagAddCmd.MarkFlagRequired("tag"); err != nil {
+ //panic(err)
+ //}
+
+}
diff --git a/cmd/tag/remove.go b/cmd/tag/remove.go
new file mode 100644
index 0000000..ca0f1b4
--- /dev/null
+++ b/cmd/tag/remove.go
@@ -0,0 +1,87 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+
+*/
+package tag
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/spf13/cobra"
+ "github.com/scrotadamus/ghligh/document"
+)
+
+var tagRemoveCmd = &cobra.Command{
+ Use: "remove",
+ Short: "remove ghligh tags from a pdf files using regex",
+ Long: `A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+
+ if regex == "" && exact == "" {
+ fmt.Fprintf(os.Stderr, "either regex or exact must be set with --regex or --exact\n")
+ os.Exit(1)
+ }
+
+ nosafe, err := cmd.Flags().GetBool("nosafe")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ // just a little hack -> boundaries = nosafe ? "" : `\b`
+ boundaries := map[bool]string{true: "", false: `\b`}[nosafe]
+ regex = formatRegex(regex, boundaries)
+
+ // if exact set overwrite regex
+ if exact != "" {
+ regex = `^` + exact + `$`
+ }
+
+ for _, file := range(args){
+ doc, err := document.Open(file)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ continue
+ }
+
+ tags := regexSlice(regex, doc.GetTags())
+ removedTags := doc.RemoveTags(tags)
+ doc.Save()
+ doc.Close()
+
+ fmt.Printf("removed %d tags from %s\n", removedTags, doc.Path)
+ }
+
+ },
+}
+
+func init() {
+ TagCmd.AddCommand(tagRemoveCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // removetagsCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // removetagsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ tagRemoveCmd.Flags().StringVarP(&regex, "regex", "r", "", "regex")
+ tagRemoveCmd.Flags().StringVarP(&exact, "exact", "e", "", "exact")
+ tagRemoveCmd.Flags().BoolP("nosafe", "", false, "don't use safe boundaries around regex")
+
+ //if err := tagRemoveCmd.MarkFlagRequired("regex"); err != nil {
+ //panic(err)
+ //}
+}
diff --git a/cmd/tag/show.go b/cmd/tag/show.go
new file mode 100644
index 0000000..3d8ae62
--- /dev/null
+++ b/cmd/tag/show.go
@@ -0,0 +1,82 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+
+*/
+package tag
+
+import (
+ "fmt"
+ "os"
+
+ "encoding/json"
+ "github.com/spf13/cobra"
+ "github.com/scrotadamus/ghligh/document"
+)
+
+var tagShowCmd = &cobra.Command{
+ Use: "show",
+ Short: "show ghligh tags of pdf files [json]",
+ Long: `A longer description that spans multiple lines and likely contains examples
+and usage of using your command. For example:
+
+Cobra is a CLI library for Go that empowers applications.
+This application is a tool to generate the needed files
+to quickly create a Cobra application.`,
+ Run: func(cmd *cobra.Command, args []string) {
+ if len(args) == 0 {
+ cmd.Help()
+ return
+ }
+
+ indent, err := cmd.Flags().GetBool("indent")
+ if err != nil {
+ cmd.Help()
+ return
+ }
+
+ exportTags := make(map[string][]string)
+ for _, file := range(args){
+ doc, err := document.Open(file)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ continue
+ }
+
+ if regex != "" {
+ regex = formatRegex(regex, "")
+ }
+ tags := regexSlice(regex, doc.GetTags())
+
+ exportTags[doc.Path] = tags
+
+ doc.Close()
+
+ }
+
+ var jsonBytes []byte
+ if indent {
+ jsonBytes, err = json.MarshalIndent(exportTags, "", " ")
+ } else {
+ jsonBytes, err = json.Marshal(exportTags)
+ }
+ if err != nil {
+ panic(err)
+ }
+ fmt.Println(string(jsonBytes))
+ },
+}
+
+func init() {
+ TagCmd.AddCommand(tagShowCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // showtagsCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ tagShowCmd.Flags().BoolP("indent", "i", false, "indent the json data")
+ tagShowCmd.Flags().StringVarP(&regex, "regex", "r", "", "regex")
+}
diff --git a/cmd/tag/tag.go b/cmd/tag/tag.go
new file mode 100644
index 0000000..30e41c8
--- /dev/null
+++ b/cmd/tag/tag.go
@@ -0,0 +1,63 @@
+/*
+Copyright © 2025 NAME HERE <EMAIL ADDRESS>
+
+*/
+package tag
+
+import (
+ "regexp"
+ "github.com/spf13/cobra"
+)
+
+
+var tags []string
+
+var regex, exact string
+
+func formatRegex(r string, boundaries string) string {
+ //return `\b` + r + `\b`
+ return boundaries + r + boundaries
+}
+
+func regexSlice(regex string, slice []string) []string {
+ if regex == "" {
+ return slice
+ }
+
+ var newSlice []string
+ re, err := regexp.Compile(regex)
+ if err != nil {
+ panic(err)
+ }
+ for _, s := range(slice){
+ if re.MatchString(s){
+ newSlice = append(newSlice, s)
+ }
+ }
+
+ return newSlice
+}
+
+// tagCmd represents the tag command
+var TagCmd = &cobra.Command{
+ Use: "tag",
+ Short: "manage pdf tags",
+ Long: `a tag is a string you can attach to a pdf
+.`,
+}
+
+func init() {
+ //rootCmd.AddCommand(tagCmd)
+
+ // Here you will define your flags and configuration settings.
+
+ // Cobra supports Persistent Flags which will work for this command
+ // and all subcommands, e.g.:
+ // tagCmd.PersistentFlags().String("foo", "", "A help for foo")
+
+ // Cobra supports local flags which will only run when this command
+ // is called directly, e.g.:
+ // tagCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
+ // TODO add to "add" command
+ // tagCmd.Flags().StringArrayVarP(&tags, "tag", "t", []string{}, "Tag da associare ai file (può essere usato più volte)")
+}