aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2025-01-06 02:54:52 +0700
committerShulhan <ms@kilabit.info>2025-01-07 00:13:55 +0700
commit85ae94f62b75372943a8ffdd705ce932a7849a8d (patch)
treeff55363d9e47729ce5d3c0be9fafa54d2cbd2e30
parent6593f4d2069790c73595f14b3312a8d83e61760e (diff)
downloadciigo-85ae94f62b75372943a8ffdd705ce932a7849a8d.tar.xz
all: auto convert markup when HTTP client request GET to HTML file
In development mode, where [ServeOptions.IsDevelopment] is set to true or when running "ciigo serve", the ciigo HTTP server will check if the new markup file is newer than HTML file when user press refresh or reload on the browser. If its newer, it will convert the markup file and return the new content of HTML file.
-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)
+ }
+}