diff options
| -rw-r--r-- | cmd/ciigo/main.go | 2 | ||||
| -rw-r--r-- | convert_options.go | 15 | ||||
| -rw-r--r-- | file_markup.go | 16 | ||||
| -rw-r--r-- | go.mod | 11 | ||||
| -rw-r--r-- | go.sum | 18 | ||||
| -rw-r--r-- | watcher.go | 168 | ||||
| -rw-r--r-- | watcher_test.go | 31 |
7 files changed, 120 insertions, 141 deletions
diff --git a/cmd/ciigo/main.go b/cmd/ciigo/main.go index a4b734c..ceb6389 100644 --- a/cmd/ciigo/main.go +++ b/cmd/ciigo/main.go @@ -52,7 +52,7 @@ func main() { convertOpts = ciigo.ConvertOptions{ Root: flag.Arg(1), HTMLTemplate: *htmlTemplate, - Exclude: *exclude, + Exclude: []string{*exclude}, } err error ) diff --git a/convert_options.go b/convert_options.go index 0423644..5879151 100644 --- a/convert_options.go +++ b/convert_options.go @@ -6,6 +6,7 @@ package ciigo import ( "fmt" "regexp" + "strings" ) const ( @@ -22,7 +23,7 @@ type ConvertOptions struct { // Exclude define regular expresion to exclude certain paths from // being scanned. - Exclude string + Exclude []string // HTMLTemplate the HTML template to be used when converting markup // file into HTML. @@ -41,16 +42,20 @@ func (opts *ConvertOptions) init() (err error) { if len(opts.Root) == 0 { opts.Root = DefaultRoot } - if len(opts.Exclude) > 0 { - var re *regexp.Regexp + for _, str := range opts.Exclude { + str = strings.TrimSpace(str) + if len(str) == 0 { + continue + } - re, err = regexp.Compile(opts.Exclude) + var re *regexp.Regexp + re, err = regexp.Compile(str) if err != nil { return fmt.Errorf(`%s: %w`, logp, err) } opts.excRE = append(opts.excRE, re) - defExcludes = append(defExcludes, opts.Exclude) + defExcludes = append(defExcludes, str) } return nil } diff --git a/file_markup.go b/file_markup.go index b2d051f..95785bb 100644 --- a/file_markup.go +++ b/file_markup.go @@ -20,7 +20,6 @@ const ( type FileMarkup struct { info os.FileInfo // info contains FileInfo of markup file. - basePath string // Full path to file without markup extension. path string // Full path to markup file. pathHTML string // path to HTML file. @@ -30,11 +29,7 @@ type FileMarkup struct { // NewFileMarkup create new FileMarkup instance form file in "filePath". // The "fi" option is optional, if its nil it will Stat-ed manually. func NewFileMarkup(filePath string, fi os.FileInfo) (fmarkup *FileMarkup, err error) { - var ( - logp = `NewFileMarkup` - - ext string - ) + var logp = `NewFileMarkup` if len(filePath) == 0 { return nil, fmt.Errorf(`%s: empty path`, logp) @@ -46,17 +41,16 @@ func NewFileMarkup(filePath string, fi os.FileInfo) (fmarkup *FileMarkup, err er } } - ext = strings.ToLower(filepath.Ext(filePath)) + var ext = strings.ToLower(filepath.Ext(filePath)) + var basePath = strings.TrimSuffix(filePath, ext) fmarkup = &FileMarkup{ - path: filePath, info: fi, - basePath: strings.TrimSuffix(filePath, ext), + path: filePath, + pathHTML: basePath + `.html`, kind: markupKind(ext), } - fmarkup.pathHTML = fmarkup.basePath + `.html` - return fmarkup, nil } @@ -3,18 +3,19 @@ module git.sr.ht/~shulhan/ciigo -go 1.22.0 +go 1.23.4 require ( - git.sr.ht/~shulhan/asciidoctor-go v0.6.1 - git.sr.ht/~shulhan/pakakeh.go v0.58.1 + git.sr.ht/~shulhan/asciidoctor-go v0.6.2-0.20250106025231-ed20fc1c23e8 + git.sr.ht/~shulhan/pakakeh.go v0.59.0 github.com/yuin/goldmark v1.7.8 github.com/yuin/goldmark-meta v1.1.0 ) require ( - golang.org/x/net v0.32.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.29.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -1,15 +1,17 @@ -git.sr.ht/~shulhan/asciidoctor-go v0.6.1 h1:uXRFNYV+iiI8XLo8Bp1ZZM/orULkAqD2lfYtra/PzhU= -git.sr.ht/~shulhan/asciidoctor-go v0.6.1/go.mod h1:QeBhtZf4GjRgENlX2Xe/TXIupqZJUtH6U89YtOvZ08Q= -git.sr.ht/~shulhan/pakakeh.go v0.58.1 h1:KBb/6rT/IjBOP1MqjI6uKVw/miTjTuwR27e8oWzz3es= -git.sr.ht/~shulhan/pakakeh.go v0.58.1/go.mod h1:QOiVaVWOilYaB+OlQtQfZo9uSvSVSVP1r8s2zve6imY= +git.sr.ht/~shulhan/asciidoctor-go v0.6.2-0.20250106025231-ed20fc1c23e8 h1:8qzcug5hnyz1QPVxCf9J3JhIzPSnc+zUK5VQachVQf0= +git.sr.ht/~shulhan/asciidoctor-go v0.6.2-0.20250106025231-ed20fc1c23e8/go.mod h1:QOHiOctWAPcVlTMOKnnwmpnLrobkv7oNAPAf4gZjtWs= +git.sr.ht/~shulhan/pakakeh.go v0.59.0 h1:haUyuusXQ0YVgHbbhi46Nkl/CEPhRe+cIJz22kQFGuM= +git.sr.ht/~shulhan/pakakeh.go v0.59.0/go.mod h1:UsrvzhSUJZpm1Aub99RI/XwtNnbBlJmaNoWWd9++VyI= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -4,34 +4,35 @@ package ciigo import ( - "fmt" "log" + "os" "path/filepath" - "strings" "time" "git.sr.ht/~shulhan/pakakeh.go/lib/clise" - "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" + "git.sr.ht/~shulhan/pakakeh.go/lib/watchfs/v2" ) // watcher watch for changes on all markup files and convert them // automatically to HTML. type watcher struct { changes *clise.Clise - watchDir *memfs.DirWatcher - watchTemplate *memfs.Watcher + watchDir *watchfs.DirWatcher + watchTemplate *watchfs.FileWatcher converter *Converter - // fileMarkups contains all markup files found inside "dir". + // fileMarkups contains all markup files found inside the + // [options.Root] directory recursively. // Its used to convert all markup files when the template file // changes. fileMarkups map[string]*FileMarkup - dir string + opts watchfs.DirWatcherOptions } // newWatcher create a watcher that monitor every files changes in directory -// "dir" for new, modified, and deleted markup files and HTML template file. +// [options.Root] for new, modified, and deleted markup files and HTML +// template file. // // The watcher depends on Converter to convert the markup to HTML using // the HTML template in Converter. @@ -43,58 +44,63 @@ type watcher struct { // +-- watchHTMLTemplate +--> DELETE --> Converter.htmlTemplateUseInternal() // | // +--> UPDATE --> Converter.convertFileMarkups() -func newWatcher(converter *Converter, convertOpts ConvertOptions) (w *watcher, err error) { - var ( - logp = `newWatcher` - ) - +func newWatcher( + converter *Converter, convertOpts ConvertOptions, +) (w *watcher, err error) { w = &watcher{ - dir: convertOpts.Root, converter: converter, changes: clise.New(1), } - w.watchDir = &memfs.DirWatcher{ - Options: memfs.Options{ - Root: convertOpts.Root, - Includes: []string{ - `.*\.adoc$`, - `.*\.md$`, - }, - Excludes: []string{ - `^\..*`, - `node_modules/.*`, - `vendor/.*`, - }, + w.opts = watchfs.DirWatcherOptions{ + FileWatcherOptions: watchfs.FileWatcherOptions{ + File: filepath.Join(convertOpts.Root, `.ciigo_rescan`), + Interval: time.Second, + }, + Root: convertOpts.Root, + Includes: []string{ + `.*\.(adoc|md)$`, + }, + Excludes: []string{ + `^\..*`, + `node_modules/.*`, + `vendor/.*`, }, - Delay: time.Second, } - if len(convertOpts.Exclude) > 0 { - w.watchDir.Options.Excludes = append(w.watchDir.Options.Excludes, convertOpts.Exclude) - } + w.opts.Excludes = append(w.opts.Excludes, convertOpts.Exclude...) - w.fileMarkups, err = listFileMarkups(convertOpts.Root, convertOpts.excRE) + w.watchDir, err = watchfs.WatchDir(w.opts) if err != nil { - return nil, fmt.Errorf(`%s: %w`, logp, err) + return nil, err } + w.scanFileMarkup() + return w, nil } -// start watching for changes. -func (w *watcher) start() (err error) { - err = w.watchDir.Start() - if err != nil { - return fmt.Errorf(`start: %w`, err) +func (w *watcher) scanFileMarkup() { + w.fileMarkups = make(map[string]*FileMarkup) + var files = w.watchDir.Files() + for path, fi := range files { + fmarkup, err := NewFileMarkup(path, fi) + if err != nil { + continue + } + w.fileMarkups[path] = fmarkup } +} +// start watching for changes. +func (w *watcher) start() (err error) { go w.watchFileMarkup() if len(w.converter.htmlTemplate) > 0 { - w.watchTemplate, err = memfs.NewWatcher(w.converter.htmlTemplate, 0) - if err != nil { - return fmt.Errorf(`start: %w`, err) + var opts = watchfs.FileWatcherOptions{ + File: w.converter.htmlTemplate, + Interval: 5 * time.Second, } + w.watchTemplate = watchfs.WatchFile(opts) go w.watchHTMLTemplate() } return nil @@ -107,71 +113,56 @@ func (w *watcher) stop() { } } -// watchFileMarkup watch the markup files inside the "content" directory, -// and re-generate them into HTML file when changed. +// watchFileMarkup watch the file ".ciigo_rescan" inside the "content" +// directory and reconvert all the markup into HTML files when its changes. func (w *watcher) watchFileMarkup() { var ( logp = `watchFileMarkup` - ns memfs.NodeState + listfi []os.FileInfo fmarkup *FileMarkup - ext string err error ok bool ) - for ns = range w.watchDir.C { - ext = strings.ToLower(filepath.Ext(ns.Node.SysPath)) - if !isExtensionMarkup(ext) { + for listfi = range w.watchDir.C { + if len(listfi) == 0 { continue } - switch ns.State { - case memfs.FileStateDeleted: - log.Printf(`%s: %q deleted`, logp, ns.Node.SysPath) - fmarkup, ok = w.fileMarkups[ns.Node.SysPath] - if ok { - delete(w.fileMarkups, ns.Node.SysPath) - w.changes.Push(fmarkup) - } - continue + for _, fi := range listfi { + var name = fi.Name() - case memfs.FileStateCreated: - log.Printf(`%s: %s created`, logp, ns.Node.SysPath) - fmarkup, err = NewFileMarkup(ns.Node.SysPath, nil) - if err != nil { - log.Printf("%s: %s\n", logp, err) + if fi.Size() == watchfs.FileFlagDeleted { + log.Printf(`%s: %q deleted`, logp, name) + fmarkup, ok = w.fileMarkups[name] + if ok { + delete(w.fileMarkups, name) + w.changes.Push(fmarkup) + } continue } - w.fileMarkups[ns.Node.SysPath] = fmarkup - - case memfs.FileStateUpdateMode: - log.Printf(`%s: %s mode updated`, logp, ns.Node.SysPath) - continue - - case memfs.FileStateUpdateContent: - log.Printf(`%s: %s content updated`, logp, ns.Node.SysPath) - fmarkup = w.fileMarkups[ns.Node.SysPath] + fmarkup = w.fileMarkups[name] if fmarkup == nil { - log.Printf("%s: %s not found\n", logp, ns.Node.SysPath) - - fmarkup, err = NewFileMarkup(ns.Node.SysPath, nil) + log.Printf(`%s: %s created`, logp, name) + fmarkup, err = NewFileMarkup(name, nil) if err != nil { - log.Printf("%s: %s\n", logp, err) + log.Printf(`%s: %s`, logp, err) continue } - w.fileMarkups[ns.Node.SysPath] = fmarkup + w.fileMarkups[name] = fmarkup } - } - err = w.converter.ToHTMLFile(fmarkup) - if err != nil { - log.Printf(`%s: %s`, logp, err) - } + err = w.converter.ToHTMLFile(fmarkup) + if err != nil { + log.Printf(`%s: %s`, logp, err) + } - w.changes.Push(fmarkup) + log.Printf(`%s: %q converted`, logp, fmarkup.path) + w.changes.Push(fmarkup) + } } } @@ -181,19 +172,18 @@ func (w *watcher) watchHTMLTemplate() { var ( logp = `watchHTMLTemplate` - ns memfs.NodeState err error ) - for ns = range w.watchTemplate.C { - if ns.State == memfs.FileStateDeleted { - log.Printf("%s: HTML template file %q has been deleted\n", - logp, ns.Node.SysPath) + for fi := range w.watchTemplate.C { + if fi == nil { + log.Printf(`%s: HTML template file has been deleted`, logp) err = w.converter.htmlTemplateUseInternal() - } else { - log.Printf(`%s: recompiling HTML template %q ...`, logp, ns.Node.SysPath) - err = w.converter.SetHTMLTemplateFile(w.converter.htmlTemplate) + continue } + + log.Printf(`%s: recompiling HTML template %q ...`, logp, fi.Name()) + err = w.converter.SetHTMLTemplateFile(w.converter.htmlTemplate) if err != nil { log.Printf(`%s: %s`, logp, err) continue diff --git a/watcher_test.go b/watcher_test.go index 063eb8f..9067d57 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -40,8 +40,7 @@ func TestWatcher(t *testing.T) { if err != nil { t.Fatal(err) } - - testWatcher.watchDir.Delay = 100 * time.Millisecond + testWatcher.opts.FileWatcherOptions.Interval = 50 * time.Millisecond err = testWatcher.start() if err != nil { @@ -55,7 +54,7 @@ func TestWatcher(t *testing.T) { t.Fatal(err) } - var pathFileMarkup = filepath.Join(testWatcher.dir, `test.adoc`) + var pathFileMarkup = filepath.Join(testWatcher.opts.Root, `test.adoc`) t.Run(`testAdocCreate`, func(tt *testing.T) { testAdocCreate(tt, testWatcher, tdata, pathFileMarkup) @@ -67,7 +66,7 @@ func TestWatcher(t *testing.T) { testAdocDelete(tt, testWatcher, pathFileMarkup) }) - pathFileMarkup = filepath.Join(testWatcher.dir, `test.md`) + pathFileMarkup = filepath.Join(testWatcher.opts.Root, `test.md`) t.Run(`testMarkdownCreate`, func(tt *testing.T) { testMarkdownCreate(tt, testWatcher, tdata, pathFileMarkup) @@ -91,14 +90,11 @@ func testAdocCreate(t *testing.T, testWatcher *watcher, tdata *test.Data, pathFi gotBody []byte ) - // Let the OS sync the file system before we create new file, - // otherwise the modtime for fs.Root does not changes. - time.Sleep(100 * time.Millisecond) - err = os.WriteFile(pathFile, expBody, 0600) if err != nil { t.Fatal(err) } + testWatcher.watchDir.ForceRescan() got = testWatcher.waitChanges() @@ -124,14 +120,11 @@ func testAdocUpdate(t *testing.T, testWatcher *watcher, tdata *test.Data, pathFi gotBody []byte ) - // Let the OS sync the file system before we create new file, - // otherwise the modtime for fs.Root does not changes. - time.Sleep(100 * time.Millisecond) - err = os.WriteFile(pathFile, expBody, 0600) if err != nil { t.Fatal(err) } + testWatcher.watchDir.ForceRescan() got = testWatcher.waitChanges() @@ -159,6 +152,7 @@ func testAdocDelete(t *testing.T, testWatcher *watcher, pathFile string) { if err != nil { t.Fatal(err) } + testWatcher.watchDir.ForceRescan() got = testWatcher.waitChanges() @@ -178,14 +172,11 @@ func testMarkdownCreate(t *testing.T, testWatcher *watcher, tdata *test.Data, pa gotBody []byte ) - // Let the OS sync the file system before we create new file, - // otherwise the modtime for fs.Root does not changes. - time.Sleep(100 * time.Millisecond) - err = os.WriteFile(pathFileMarkdown, body, 0600) if err != nil { t.Fatal(err) } + testWatcher.watchDir.ForceRescan() got = testWatcher.waitChanges() @@ -211,14 +202,11 @@ func testMarkdownUpdate(t *testing.T, testWatcher *watcher, tdata *test.Data, pa gotBody []byte ) - // Let the OS sync the file system before we create new file, - // otherwise the modtime for fs.Root does not changes. - time.Sleep(100 * time.Millisecond) - err = os.WriteFile(pathFileMarkdown, body, 0600) if err != nil { t.Fatal(err) } + testWatcher.watchDir.ForceRescan() got = testWatcher.waitChanges() @@ -241,12 +229,11 @@ func testMarkdownDelete(t *testing.T, testWatcher *watcher, pathFileMarkdown str gotIsExist bool ) - time.Sleep(100 * time.Millisecond) - err = os.Remove(pathFileMarkdown) if err != nil { t.Fatal(err) } + testWatcher.watchDir.ForceRescan() got = testWatcher.waitChanges() test.Assert(t, `md file updated`, pathFileMarkdown, got.path) |
