diff options
| -rw-r--r-- | converter.go | 62 | ||||
| -rw-r--r-- | server.go | 60 | ||||
| -rw-r--r-- | server_test.go | 203 | ||||
| -rw-r--r-- | testdata/onGet_template.gohtml | 1 | ||||
| -rw-r--r-- | testdata/onGet_test.txt | 99 | ||||
| -rw-r--r-- | watcher.go | 59 | ||||
| -rw-r--r-- | watcher_test.go | 40 |
7 files changed, 505 insertions, 19 deletions
diff --git a/converter.go b/converter.go index da1aa8e..57695e7 100644 --- a/converter.go +++ b/converter.go @@ -84,29 +84,13 @@ func (converter *Converter) convertFileMarkups(fileMarkups map[string]*FileMarku var ( logp = `convertFileMarkups` - fmarkup *FileMarkup - htmlInfo os.FileInfo - htmlModtime time.Time - err error - skip bool + fmarkup *FileMarkup + err error ) for _, fmarkup = range fileMarkups { - skip = true if !isForce { - htmlInfo, _ = os.Stat(fmarkup.pathHTML) - if htmlInfo == nil { - // HTML file may not exist. - skip = false - } else { - htmlModtime = htmlInfo.ModTime() - if converter.htmlTemplateModtime.After(htmlModtime) { - skip = false - } else if fmarkup.info.ModTime().After(htmlModtime) { - skip = false - } - } - if skip { + if !converter.shouldConvert(fmarkup) { continue } } @@ -230,3 +214,43 @@ func (converter *Converter) markdownToHTML(fmarkup *FileMarkup) (fhtml *fileHTML return fhtml, nil } + +// shouldConvert will return true if the file markup fmarkup needs to be +// converted to HTML. +// It return true if the HTML file not exist or the template or markup file is +// newer than the HTML file. +func (converter *Converter) shouldConvert(fmarkup *FileMarkup) bool { + var fi os.FileInfo + fi, _ = os.Stat(fmarkup.pathHTML) + if fi == nil { + // HTML file may not exist. + return true + } + + var htmlModtime = fi.ModTime() + var err error + + if len(converter.htmlTemplate) != 0 { + fi, err = os.Stat(converter.htmlTemplate) + if err != nil { + // The template file may has been deleted. + return true + } + + if fi.ModTime().After(htmlModtime) { + converter.htmlTemplateModtime = fi.ModTime() + return true + } + } + + fi, err = os.Stat(fmarkup.path) + if err != nil { + // The markup file may has been deleted. + return false + } + if fi.ModTime().After(htmlModtime) || fmarkup.info.Size() != fi.Size() { + fmarkup.info = fi + return true + } + return false +} @@ -8,6 +8,8 @@ import ( "fmt" "html/template" "log" + "net/http" + "path" "strings" libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" @@ -50,6 +52,9 @@ func (ciigo *Ciigo) InitHTTPServer(opts ServeOptions) (err error) { Address: opts.Address, EnableIndexHTML: opts.EnableIndexHTML, } + if opts.IsDevelopment { + httpdOpts.HandleFS = ciigo.onGet + } ciigo.HTTPServer, err = libhttp.NewServer(httpdOpts) if err != nil { @@ -164,3 +169,58 @@ func (ciigo *Ciigo) onSearch(epr *libhttp.EndpointRequest) (resBody []byte, err return resBody, nil } + +// onGet when user reload the page from browser, inspect the HTML file by +// checking if its older that the adoc. +// If yes, it will auto convert the adoc and return the new content of HTML +// files. +func (ciigo *Ciigo) onGet( + node *memfs.Node, _ http.ResponseWriter, req *http.Request, +) (out *memfs.Node) { + var ( + logp = `onGet` + file string + ) + + if node == nil { + file = req.URL.Path + } else { + if node.IsDir() { + file = path.Join(node.Path, `index.html`) + } else { + if len(req.URL.Path) > len(node.Path) { + file = req.URL.Path + } else { + file = node.Path + } + } + } + if file[len(file)-1] == '/' { + file = path.Join(file, `index.html`) + } + + var ( + fmarkup *FileMarkup + isNew bool + ) + fmarkup, isNew = ciigo.watcher.getFileMarkupByHTML(file) + if fmarkup == nil { + // File is not HTML or no markup files created from it. + return node + } + var err error + if isNew || ciigo.converter.shouldConvert(fmarkup) { + err = ciigo.converter.ToHTMLFile(fmarkup) + if err != nil { + log.Printf(`%s: failed to convert markup file %q: %s`, + logp, fmarkup.path, err) + return node + } + } + out, err = ciigo.serveOpts.Mfs.Get(file) + if err != nil { + log.Printf(`%s: failed to get %q: %s`, logp, file, err) + return node + } + return out +} diff --git a/server_test.go b/server_test.go new file mode 100644 index 0000000..fab2e4f --- /dev/null +++ b/server_test.go @@ -0,0 +1,203 @@ +// SPDX-FileCopyrightText: 2024 Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later + +package ciigo_test + +import ( + "net/http" + "os" + "path/filepath" + "regexp" + "testing" + + "git.sr.ht/~shulhan/ciigo" + "git.sr.ht/~shulhan/pakakeh.go/lib/test" + "git.sr.ht/~shulhan/pakakeh.go/lib/test/httptest" +) + +func TestCiigoOnGet(t *testing.T) { + var tdata *test.Data + var err error + + tdata, err = test.LoadData(`testdata/onGet_test.txt`) + if err != nil { + t.Fatal(err) + } + + var dirRoot = t.TempDir() + + // Create one adoc, one md, and one html file. + for _, name := range []string{`one.adoc`, `two.md`, `three.html`} { + var file = filepath.Join(dirRoot, name) + err = os.WriteFile(file, tdata.Input[name], 0600) + if err != nil { + t.Fatal(err) + } + } + + var cigo = ciigo.Ciigo{} + + var serveOpts = ciigo.ServeOptions{ + Address: `127.0.0.1:11083`, + ConvertOptions: ciigo.ConvertOptions{ + Root: dirRoot, + HTMLTemplate: `testdata/onGet_template.gohtml`, + }, + } + err = cigo.InitHTTPServer(serveOpts) + if err != nil { + t.Fatal(err) + } + + var redactLastUpdated = regexp.MustCompile(`Last updated.*`) + + var listCase = []struct { + desc string + req httptest.SimulateRequest + expBody []byte + }{{ + desc: `GET /one.html`, + req: httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/one.html`, + }, + expBody: tdata.Output[`one.html`], + }, { + desc: `GET /two.html`, + req: httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/two.html`, + }, + expBody: tdata.Output[`two.html`], + }, { + desc: `GET /three.html`, + req: httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/three.html`, + }, + expBody: tdata.Output[`three.html`], + }} + var result *httptest.SimulateResult + for _, tcase := range listCase { + result, err = httptest.Simulate(cigo.HTTPServer.ServeHTTP, + &tcase.req) + if err != nil { + t.Fatal(err) + } + var gotBody = redactLastUpdated.ReplaceAll( + result.ResponseBody, []byte("[REDACTED]")) + test.Assert(t, tcase.desc, string(tcase.expBody), + string(gotBody)) + } + + // On next test, we create markup file for three.html. + // The output from HTML should changes. + + t.Run(`On markup created for HTML`, func(t *testing.T) { + var name = `three.adoc` + var file = filepath.Join(dirRoot, name) + err = os.WriteFile(file, tdata.Input[name], 0600) + if err != nil { + t.Fatal(err) + } + + var req = httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/three.html`, + } + + var result *httptest.SimulateResult + result, err = httptest.Simulate(cigo.HTTPServer.ServeHTTP, &req) + if err != nil { + t.Fatal(err) + } + + var expBody = tdata.Output[`new_three.html`] + var gotBody = redactLastUpdated.ReplaceAll( + result.ResponseBody, []byte("[REDACTED]")) + test.Assert(t, `new_three.html`, string(expBody), string(gotBody)) + }) + + t.Run(`On markup updated`, func(t *testing.T) { + var file = filepath.Join(dirRoot, `one.adoc`) + err = os.WriteFile(file, tdata.Input[`update_one.adoc`], 0600) + if err != nil { + t.Fatal(err) + } + + var req = httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/one.html`, + } + var result *httptest.SimulateResult + result, err = httptest.Simulate(cigo.HTTPServer.ServeHTTP, &req) + if err != nil { + t.Fatal(err) + } + + var expBody = tdata.Output[`update_one.html`] + var gotBody = redactLastUpdated.ReplaceAll( + result.ResponseBody, []byte("[REDACTED]")) + test.Assert(t, `body`, string(expBody), string(gotBody)) + }) + + t.Run(`On new directory with markup`, func(t *testing.T) { + var dirJournal = filepath.Join(dirRoot, `journal`) + err = os.MkdirAll(dirJournal, 0755) + if err != nil { + t.Fatal(err) + } + + var journalIndex = filepath.Join(dirJournal, `index.adoc`) + err = os.WriteFile(journalIndex, + tdata.Input[`/journal/index.adoc`], 0600) + if err != nil { + t.Fatal(err) + } + + var req = httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/journal/`, + } + var result *httptest.SimulateResult + result, err = httptest.Simulate(cigo.HTTPServer.ServeHTTP, &req) + if err != nil { + t.Fatal(err) + } + + var expBody = tdata.Output[`/journal/index.html`] + var gotBody = redactLastUpdated.ReplaceAll( + result.ResponseBody, []byte("[REDACTED]")) + test.Assert(t, `body`, string(expBody), string(gotBody)) + }) + + t.Run(`On new directory request without slash`, func(t *testing.T) { + var dirJournal2 = filepath.Join(dirRoot, `journal2`) + err = os.MkdirAll(dirJournal2, 0755) + if err != nil { + t.Fatal(err) + } + + var journalIndexAdoc = filepath.Join(dirJournal2, `index.adoc`) + err = os.WriteFile(journalIndexAdoc, + tdata.Input[`/journal2/index.adoc`], 0600) + if err != nil { + t.Fatal(err) + } + + var req = httptest.SimulateRequest{ + Method: http.MethodGet, + Path: `/journal2`, + } + var result *httptest.SimulateResult + result, err = httptest.Simulate(cigo.HTTPServer.ServeHTTP, &req) + if err != nil { + t.Fatal(err) + } + + var expBody = tdata.Output[`/journal2/index.html`] + var gotBody = redactLastUpdated.ReplaceAll( + result.ResponseBody, []byte("[REDACTED]")) + test.Assert(t, `body`, string(expBody), string(gotBody)) + }) +} diff --git a/testdata/onGet_template.gohtml b/testdata/onGet_template.gohtml new file mode 100644 index 0000000..dd786b2 --- /dev/null +++ b/testdata/onGet_template.gohtml @@ -0,0 +1 @@ +{{.Body}} diff --git a/testdata/onGet_test.txt b/testdata/onGet_test.txt new file mode 100644 index 0000000..048ac37 --- /dev/null +++ b/testdata/onGet_test.txt @@ -0,0 +1,99 @@ + +>>> one.adoc += One + +<<< one.html +<div id="header"> +<h1>One</h1> +</div> +<div id="content"> +</div> +<div id="footer"> +<div id="footer-text"> +[REDACTED] +</div> +</div> + +>>> two.md +# Two + +<<< two.html +<h1>Two</h1> + + + +>>> three.html +<html> +</html> + +<<< three.html +<html> +</html> + +>>> three.adoc += Three + +<<< new_three.html +<div id="header"> +<h1>Three</h1> +</div> +<div id="content"> +</div> +<div id="footer"> +<div id="footer-text"> +[REDACTED] +</div> +</div> + +>>> update_one.adoc += One + +Updated. + +<<< update_one.html +<div id="header"> +<h1>One</h1> +</div> +<div id="content"> +<div class="paragraph"> +<p>Updated.</p> +</div> +</div> +<div id="footer"> +<div id="footer-text"> +[REDACTED] +</div> +</div> + + +>>> /journal/index.adoc += Journal + +Hello world! + +<<< /journal/index.html +<div id="header"> +<h1>Journal</h1> +</div> +<div id="content"> +<div class="paragraph"> +<p>Hello world!</p> +</div> +</div> +<div id="footer"> +<div id="footer-text"> +[REDACTED] +</div> +</div> + +>>> /journal2/index.adoc += Journal 2 + +Hello world! + +<<< /journal2/index.html +<a href="/journal2/">Found</a>. + + + +>>> END @@ -7,6 +7,7 @@ import ( "log" "os" "path/filepath" + "strings" "time" "git.sr.ht/~shulhan/pakakeh.go/lib/clise" @@ -79,6 +80,64 @@ func newWatcher( return w, nil } +// getFileMarkupByHTML get the file markup based on the HTML file name. +func (w *watcher) getFileMarkupByHTML(fileHTML string) ( + fmarkup *FileMarkup, isNew bool, +) { + // Use file extension to handle insensitive cases of '.html' suffix. + var ext = filepath.Ext(fileHTML) + if strings.ToLower(ext) != `.html` { + return nil, false + } + + var ( + pathMarkup string + ok bool + ) + pathMarkup, ok = strings.CutSuffix(fileHTML, ext) + if !ok { + return nil, false + } + pathMarkup = filepath.Join(w.opts.Root, pathMarkup) + + var pathMarkupAdoc = pathMarkup + `.adoc` + fmarkup = w.fileMarkups[pathMarkupAdoc] + if fmarkup != nil { + return fmarkup, false + } + + var pathMarkupMd = pathMarkup + `.md` + fmarkup = w.fileMarkups[pathMarkupMd] + if fmarkup != nil { + return fmarkup, false + } + + // Directly check on the file system. + + var fi os.FileInfo + var err error + fi, err = os.Stat(pathMarkupAdoc) + if err == nil { + fmarkup, err = NewFileMarkup(pathMarkupAdoc, fi) + if err != nil { + return nil, false + } + w.fileMarkups[pathMarkupAdoc] = fmarkup + return fmarkup, true + } + + fi, err = os.Stat(pathMarkupMd) + if err == nil { + fmarkup, err = NewFileMarkup(pathMarkupMd, fi) + if err != nil { + return nil, false + } + w.fileMarkups[pathMarkupMd] = fmarkup + return fmarkup, true + } + return nil, false +} + func (w *watcher) scanFileMarkup() { w.fileMarkups = make(map[string]*FileMarkup) var files = w.watchDir.Files() diff --git a/watcher_test.go b/watcher_test.go index 9067d57..1b9f3fa 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -255,3 +255,43 @@ func removeFooter(in []byte, nlast int) (out []byte) { out = bytes.Join(lines, []byte("\n")) return out } + +func TestWatcherGetFileMarkupByHTML(t *testing.T) { + var w = watcher{ + fileMarkups: map[string]*FileMarkup{ + `/markup/with/adoc/file.adoc`: &FileMarkup{ + kind: markupKindAdoc, + }, + `/markup/with/md/file.md`: &FileMarkup{ + kind: markupKindMarkdown, + }, + }, + } + + var listCase = []struct { + expFileMarkup *FileMarkup + fileHTML string + expIsNew bool + }{{ + fileHTML: `/notexist.html`, + }, { + fileHTML: `/markup/with/adoc/file.html`, + expFileMarkup: w.fileMarkups[`/markup/with/adoc/file.adoc`], + }, { + fileHTML: `/markup/with/adoc/file.HTML`, + expFileMarkup: w.fileMarkups[`/markup/with/adoc/file.adoc`], + }, { + fileHTML: `/markup/with/md/file.HTML`, + expFileMarkup: w.fileMarkups[`/markup/with/md/file.md`], + }} + + var ( + gotFileMarkup *FileMarkup + gotIsNew bool + ) + for _, tcase := range listCase { + gotFileMarkup, gotIsNew = w.getFileMarkupByHTML(tcase.fileHTML) + test.Assert(t, tcase.fileHTML, tcase.expFileMarkup, gotFileMarkup) + test.Assert(t, tcase.fileHTML+` isNew`, tcase.expIsNew, gotIsNew) + } +} |
