diff --git a/service/middleware.go b/service/middleware.go index aa57e031..31d21b75 100644 --- a/service/middleware.go +++ b/service/middleware.go @@ -63,6 +63,14 @@ func withStaticFileServer(c *gin.Context) { return } + // Content negotiation: .md suffix or Accept: text/markdown + if strings.HasSuffix(c.Request.URL.Path, ".md") { + c.Set("content_type", "markdown") + c.Request.URL.Path = strings.TrimSuffix(c.Request.URL.Path, ".md") + } else if strings.Contains(c.GetHeader("Accept"), "text/markdown") { + c.Set("content_type", "markdown") + } + // Rewrite for _, rewrite := range rewriteRules { // log.Debug("Rewrite: %s => %s", c.Request.URL.Path, rewrite.Replacement) @@ -90,6 +98,18 @@ func withStaticFileServer(c *gin.Context) { return } + // Content negotiation: serve raw markdown instead of HTML + if ct, exists := c.Get("content_type"); exists && ct == "markdown" { + raw, contentType, code, err := r.RenderRaw("markdown") + if err != nil { + c.AbortWithStatusJSON(code, gin.H{"code": code, "message": err.Error()}) + return + } + c.Data(code, contentType, []byte(raw)) + c.Done() + return + } + html, code, err := r.Render() if err != nil { if code == 301 || code == 302 { diff --git a/sui/api/request.go b/sui/api/request.go index 941fbd6d..1d889bea 100644 --- a/sui/api/request.go +++ b/sui/api/request.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" jsoniter "github.com/json-iterator/go" "github.com/yaoapp/gou/application" + "github.com/yaoapp/gou/process" "github.com/yaoapp/kun/exception" "github.com/yaoapp/kun/log" "github.com/yaoapp/yao/sui/core" @@ -194,6 +195,122 @@ func (r *Request) Render() (string, int, error) { return html, 200, nil } +// RenderRaw serves alternative content types (e.g. markdown) by invoking the +// handler declared in the page config, skipping data scripts and HTML rendering. +// "method" calls the page's backend.ts function; "process" calls a global Yao process. +func (r *Request) RenderRaw(kind string) (string, string, int, error) { + + // Load or build cache (same as Render) + var c *core.Cache = nil + if !r.Request.DisableCache() { + c = core.GetCache(r.File) + } + if c == nil { + var status int + var err error + c, status, err = r.MakeCache() + if err != nil { + return "", "", status, err + } + } + + // Guard + code, err := r.Guard(c) + if err != nil { + return "", "", code, err + } + + // Parse config to find the handler + if c.Config == "" { + return "", "", 404, fmt.Errorf("page does not support %s output", kind) + } + + var conf core.PageConfig + if err := jsoniter.UnmarshalFromString(c.Config, &conf); err != nil { + return "", "", 500, fmt.Errorf("config parse error: %s", err.Error()) + } + + // Resolve the handler from config by kind + var handler *core.PageProcess + switch kind { + case "markdown": + handler = conf.Markdown + } + + if handler == nil || (handler.Method == "" && handler.Process == "") { + return "", "", 404, fmt.Errorf("page does not support %s output", kind) + } + + // Resolve arguments + args := make([]interface{}, len(handler.In)) + for i, expr := range handler.In { + args[i] = r.resolveArg(expr) + } + + var result interface{} + + if handler.Method != "" { + // Call backend.ts function via the page's compiled script + if c.Script == nil { + return "", "", 500, fmt.Errorf("page has no backend script") + } + r.Request.Script = c.Script + result, err = c.Script.Call(r.Request, handler.Method, args...) + if err != nil { + return "", "", 500, fmt.Errorf("backend script error: %s", err.Error()) + } + } else { + // Fallback: call a global Yao process + p, err := process.Of(handler.Process, args...) + if err != nil { + return "", "", 500, fmt.Errorf("process error: %s", err.Error()) + } + result, err = p.Exec() + if err != nil { + return "", "", 500, fmt.Errorf("process exec error: %s", err.Error()) + } + } + + content := "" + switch v := result.(type) { + case string: + content = v + case []byte: + content = string(v) + default: + return "", "", 500, fmt.Errorf("handler must return string, got %T", result) + } + + contentType := "text/markdown; charset=utf-8" + return content, contentType, 200, nil +} + +// resolveArg replaces $param.*, $query.* placeholders with actual request values. +func (r *Request) resolveArg(expr string) interface{} { + if strings.HasPrefix(expr, "$param.") { + key := expr[7:] + if val, ok := r.Request.Params[key]; ok { + return val + } + return "" + } + if strings.HasPrefix(expr, "$query.") { + key := expr[7:] + if r.Request.Query.Has(key) { + return r.Request.Query.Get(key) + } + return "" + } + if strings.HasPrefix(expr, "$header.") { + key := expr[8:] + if r.Request.Headers.Has(key) { + return r.Request.Headers.Get(key) + } + return "" + } + return expr +} + // MakeCache is the cache for the page API. func (r *Request) MakeCache() (*core.Cache, int, error) { diff --git a/sui/core/page.go b/sui/core/page.go index 29fba839..f1977c84 100644 --- a/sui/core/page.go +++ b/sui/core/page.go @@ -72,6 +72,7 @@ func (page *Page) ExportConfig() string { "dataCache": page.Config.DataCache, "api": page.Config.API, "root": page.Root, + "markdown": page.Config.Markdown, }) if err != nil { diff --git a/sui/core/types.go b/sui/core/types.go index 2f43fa25..62a9f510 100644 --- a/sui/core/types.go +++ b/sui/core/types.go @@ -375,15 +375,25 @@ type PageConfig struct { // PageSetting is the struct for the page setting type PageSetting struct { - Title string `json:"title,omitempty"` - Guard string `json:"guard,omitempty"` - CacheStore string `json:"cacheStore,omitempty"` - Cache int `json:"cache,omitempty"` - Root string `json:"root,omitempty"` - DataCache int `json:"dataCache,omitempty"` - Description string `json:"description,omitempty"` - SEO *PageSEO `json:"seo,omitempty"` - API *PageAPI `json:"api,omitempty"` + Title string `json:"title,omitempty"` + Guard string `json:"guard,omitempty"` + CacheStore string `json:"cacheStore,omitempty"` + Cache int `json:"cache,omitempty"` + Root string `json:"root,omitempty"` + DataCache int `json:"dataCache,omitempty"` + Description string `json:"description,omitempty"` + SEO *PageSEO `json:"seo,omitempty"` + API *PageAPI `json:"api,omitempty"` + Markdown *PageProcess `json:"markdown,omitempty"` +} + +// PageProcess binds a handler to a content negotiation output. +// "method" calls the page's own backend.ts function (preferred); +// "process" calls a global Yao process as fallback. +type PageProcess struct { + Method string `json:"method,omitempty"` + Process string `json:"process,omitempty"` + In []string `json:"in,omitempty"` } // PageConfigRendered is the struct for the page config rendered