aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--converter.go62
-rw-r--r--server.go60
-rw-r--r--server_test.go203
-rw-r--r--testdata/onGet_template.gohtml1
-rw-r--r--testdata/onGet_test.txt99
-rw-r--r--watcher.go59
-rw-r--r--watcher_test.go40
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
+}
diff --git a/server.go b/server.go
index 3d10b5f..0688e2d 100644
--- a/server.go
+++ b/server.go
@@ -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
diff --git a/watcher.go b/watcher.go
index 4bb0a9c..3e04292 100644
--- a/watcher.go
+++ b/watcher.go
@@ -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)
+ }
+}