Compare commits

..

3 Commits

Author SHA1 Message Date
xing
9ee402af4b 优化 where解析 2023-02-22 20:07:57 +08:00
xing
00f788fad5 scanner的性能比sqlx的select好些的样子 2023-02-22 19:12:26 +08:00
xing
1ecfa19fd4 dbquery泛型化查询,似乎性能和之前也差不多的样子 2023-02-22 16:53:53 +08:00
390 changed files with 39289 additions and 16699 deletions

8
.gitignore vendored
View File

@ -1,7 +1,3 @@
.idea
/wp-go.iml
/config.yaml
err.log
/plugins/
/config.json
go.sum
wp-go.iml
config.yaml

View File

@ -1,7 +1,8 @@
FROM golang:1.22.2-alpine as gobulidIso
FROM golang:latest as gobulidIso
COPY ./ /go/src/wp-go
WORKDIR /go/src/wp-go
RUN go build -ldflags "-w" -tags netgo -o wp-go app/cmd/main.go
ENV GOPROXY="https://goproxy.cn"
RUN go build -ldflags "-w" -tags netgo -o wp-go internal/cmd/main.go
FROM alpine:latest
WORKDIR /opt/wp-go

View File

@ -1,87 +1,18 @@
## wp-go
[en readme](https://github.com/fthvgb1/wp-go/blob/master/readme_en.md)
一个go写的WordPress的前端功能比较简单只有列表页和详情页,rss2主题只有twentyfifteen和twentyseventeen两套主题插件的话只有一个简单的列表页的摘要生成和enlighter代码高亮。本身只用于展示文章及评论。要求go的版本在1.20以上,越新越好。。。
一个go写的WordPress的前端功能比较简单只有列表页和详情页,rss2主题只有twentyfifteen和twentyseventeen两套主题插件的话只有一个简单的列表页的摘要生成和enlighter代码高亮。本身只用于展示文章添加评论走的转发请求到php的WordPress。因为大量用了泛型功能所以要求go的版本在1.19及以上,越新越好。。。。
#### 特色功能
- 基本实现全站缓存,并且可防止缓存击穿
- 列表页也可以高亮语法格式化显示代码
- 简易插件扩展开发机制、配置后支持热加载更新
- 使用.so扩展主题、插件、路由等
- 丰富繁杂的配置,呃,配置是有点儿多,虽然大部分都是可选项。。。
- 多种缓存配置
- 添加评论或panic时发邮件通知包涵栈调用和请求信息
- 简单的流量限制中间件,可以限制全瞬时最大请求数量
- 简单的流量限制中间件
- 除配置文件外将所有静态资源都打包到执行文件中
- 支持密码查看且cookie信息可被php版所验证
- 支持rss2订阅
- 热更新配置、切换主题、清空缓存
- 热更新配置、清空缓存
- kill -SIGUSR1 PID 更新配置和清空缓存
- kill -SIGUSR2 PID 清空缓存
#### 运行
```
go run app/cmd/main.go [-c configpath] [-p port]
```
#### 数据显示支持程度
| 页表 | 支持程度 |
|-----|---------------------------------------------|
| 列表页 | 首页/搜索/归档/分类/标签/作者 分页列表 |
| 详情页 | 显示内容、评论并可以添加评论(转发的php处理需要配置php版的添加评论的url) |
| 侧边栏 | 支持旧版 近期文章、近期评论、规档、分类、其它操作 显示及设置, 支持新版 分类 |
#### 后台设置支持程度
- 仪表盘
- 外观
- 小工具
- 搜索
- 规档
- 近期文章
- 近期评论
- 分类
- 其它操作
- 设置-
- 常规
- 站点标题
- 副标题
- 阅读
- 博客页面至多显示数量
- Feed中显示最近数量
- 讨论
- 其他评论设置
- `启用|禁止`评论嵌套,最多嵌套层数
- 分页显示评论,每页显示评论条数,默认显示`最前/后`页
- 在每个页面顶部显示 `新旧`评论
#### 主题支持程度
| twentyfifteen | twentyseventeen |
|---------------|-----------------|
| 站点身份 | 站点身份 |
| 颜色 | 颜色 |
| 页眉图片 | 页眉媒体 |
| 背景图片 | 额外css |
| 额外css | |
#### 插件机制
分为对列表页文章数据的修改的插件和对影响整个程序表现的插件
| 列表页文章数据插件 | 整个程序表现的插件 |
|---------------------|--------------------------------------|
| digest 自动生成指定长度的摘要 | enlighter 代码高亮(需要在后台安装enlighterjs插件) |
| | hiddenLogin 隐藏登录入口 |
#### 其它
用的gin框架和sqlx,在外面封装了层查询的方法。
#### 鸣谢
<a href="https://jb.gg/OpenSourceSupport"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main) logo." width="30%"></a>
用的gin框架和sqlx,在外面封装了层查询的方法。后台可以设置的比较少,大部分设置还没打通。

View File

@ -1,293 +0,0 @@
package actions
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/app/mail"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type CommentForm struct {
CommentPostId uint64 `form:"comment_post_ID" binding:"required" json:"comment_post_ID"`
Author string `form:"author" binding:"required" label:"显示名称" json:"author"`
Email string `form:"email" binding:"required,email"`
Comment string `form:"comment" binding:"required" label:"评论" json:"comment"`
}
func PostComment(c *gin.Context) {
cli := &http.Client{
Timeout: time.Second * 3,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
data, err := c.GetRawData()
defer func() {
if err != nil {
c.Writer.WriteHeader(http.StatusConflict)
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
var v validator.ValidationErrors
if errors.As(err, &v) {
e := v.Translate(config.GetZh())
for _, v := range e {
fmt.Fprintf(c.Writer, fail, v)
return
}
} else {
c.Writer.WriteString("评论出错,请联系管理员或稍后再度")
}
}
}()
conf := config.GetConfig()
if err != nil {
logs.Error(err, "获取评论数据错误")
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
var comment CommentForm
if err = c.ShouldBind(&comment); err != nil {
return
}
c.Request.Body = io.NopCloser(bytes.NewBuffer(data))
req, err := http.NewRequest("POST", conf.PostCommentUrl, strings.NewReader(c.Request.PostForm.Encode()))
if err != nil {
logs.Error(err, "创建评论请求错误")
return
}
defer req.Body.Close()
req.Header = c.Request.Header.Clone()
home, err := url.Parse(wpconfig.GetOption("siteurl"))
if err != nil {
logs.Error(err, "解析评论接口错误")
return
}
req.Host = home.Host
res, err := cli.Do(req)
if err != nil && !errors.Is(err, http.ErrUseLastResponse) {
logs.Error(err, "请求评论接口错误")
return
}
if res.StatusCode == http.StatusFound {
for _, cookie := range res.Cookies() {
c.SetCookie(cookie.Name, cookie.Value, cookie.MaxAge, cookie.Path, cookie.Domain, cookie.Secure, cookie.HttpOnly)
}
u := res.Header.Get("Location")
up, er := url.Parse(u)
if er != nil {
err = er
return
}
cu, er := url.Parse(conf.PostCommentUrl)
if er != nil {
err = er
return
}
up.Host = cu.Host
up.Scheme = "http"
newReq, _ := http.NewRequest("GET", up.String(), nil)
newReq.Host = home.Host
newReq.Header.Set("Cookie", strings.Join(slice.Map(c.Request.Cookies(), func(t *http.Cookie) string {
return fmt.Sprintf("%s=%s", t.Name, t.Value)
}), "; "))
ress, er := http.DefaultClient.Do(newReq)
if er != nil {
err = er
return
}
cc := c.Copy()
go func() {
if gin.Mode() != gin.ReleaseMode {
return
}
id := comment.CommentPostId
if id <= 0 {
logs.Error(errors.New("获取文档id错误"), "", comment.CommentPostId)
return
}
post, err := cache.GetPostById(cc, id)
if err != nil {
logs.Error(err, "获取文档错误", id)
return
}
su := fmt.Sprintf("%s: %s[%s]发表了评论对文档[%v]的评论", wpconfig.GetOption("siteurl"), comment.Author, comment.Email, post.PostTitle)
err = mail.SendMail([]string{conf.Mail.User}, su, comment.Comment)
logs.IfError(err, "发送邮件", conf.Mail.User, su, comment)
}()
s, er := io.ReadAll(ress.Body)
if er != nil {
err = er
return
}
uuu := ""
uu, _ := url.Parse(res.Header.Get("Location"))
if up.RawQuery != "" {
cache.NewCommentCache().Set(c, up.RawQuery, string(s))
uuu = str.Join(uu.Path, "?", uu.RawQuery)
} else {
uuu = str.Join(uu.Path)
}
c.Redirect(http.StatusFound, uuu)
return
}
var r io.Reader
if res.Header.Get("Content-Encoding") == "gzip" {
r, err = gzip.NewReader(res.Body)
if err != nil {
logs.Error(err, "gzip解压错误")
return
}
} else {
r = res.Body
}
s, err := io.ReadAll(r)
if err != nil {
logs.Error(err, "读取结果错误")
return
}
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(res.StatusCode)
_, _ = c.Writer.Write(s)
}
var fail = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width">
<meta name='robots' content='max-image-preview:large, noindex, follow' />
<title>评论提交失败</title>
<style type="text/css">
html {
background: #f1f1f1;
}
body {
background: #fff;
border: 1px solid #ccd0d4;
color: #444;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
margin: 2em auto;
padding: 1em 2em;
max-width: 700px;
-webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
box-shadow: 0 1px 1px rgba(0, 0, 0, .04);
}
h1 {
border-bottom: 1px solid #dadada;
clear: both;
color: #666;
font-size: 24px;
margin: 30px 0 0 0;
padding: 0;
padding-bottom: 7px;
}
#error-page {
margin-top: 50px;
}
#error-page p,
#error-page .wp-die-message {
font-size: 14px;
line-height: 1.5;
margin: 25px 0 20px;
}
#error-page code {
font-family: Consolas, Monaco, monospace;
}
ul li {
margin-bottom: 10px;
font-size: 14px ;
}
a {
color: #0073aa;
}
a:hover,
a:active {
color: #006799;
}
a:focus {
color: #124964;
-webkit-box-shadow:
0 0 0 1px #5b9dd9,
0 0 2px 1px rgba(30, 140, 190, 0.8);
box-shadow:
0 0 0 1px #5b9dd9,
0 0 2px 1px rgba(30, 140, 190, 0.8);
outline: none;
}
.button {
background: #f3f5f6;
border: 1px solid #016087;
color: #016087;
display: inline-block;
text-decoration: none;
font-size: 13px;
line-height: 2;
height: 28px;
margin: 0;
padding: 0 10px 1px;
cursor: pointer;
-webkit-border-radius: 3px;
-webkit-appearance: none;
border-radius: 3px;
white-space: nowrap;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
vertical-align: top;
}
.button.button-large {
line-height: 2.30769231;
min-height: 32px;
padding: 0 12px;
}
.button:hover,
.button:focus {
background: #f1f1f1;
}
.button:focus {
background: #f3f5f6;
border-color: #007cba;
-webkit-box-shadow: 0 0 0 1px #007cba;
box-shadow: 0 0 0 1px #007cba;
color: #016087;
outline: 2px solid transparent;
outline-offset: 0;
}
.button:active {
background: #f3f5f6;
border-color: #7e8993;
-webkit-box-shadow: none;
box-shadow: none;
}
</style>
</head>
<body id="error-page">
<div class="wp-die-message"><p><strong>错误</strong>%s</p></div>
<p><a href='javascript:history.back()'>&laquo; 返回</a></p></body>
</html>
`

View File

@ -1,15 +0,0 @@
package actions
import (
"github.com/fthvgb1/wp-go/app/theme"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/gin-gonic/gin"
)
func ThemeHook(scene string) func(*gin.Context) {
return func(c *gin.Context) {
t := theme.GetCurrentTheme()
h := wp.NewHandle(c, scene, t)
theme.Hook(t, h)
}
}

View File

@ -1,111 +0,0 @@
package main
import (
"flag"
"fmt"
"github.com/fthvgb1/wp-go/app/ossigns"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/db"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/app/plugins/wphandle"
"github.com/fthvgb1/wp-go/app/route"
"github.com/fthvgb1/wp-go/app/theme"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/model"
"os"
"regexp"
"strings"
"time"
)
var confPath string
var address string
var intReg = regexp.MustCompile(`^\d`)
func inits() {
flag.StringVar(&confPath, "c", "config.yaml", "config file support json,yaml or url")
flag.StringVar(&address, "p", "", "listen address and port")
flag.Parse()
if address == "" && os.Getenv("PORT") == "" {
address = "80"
}
if intReg.MatchString(address) && !strings.Contains(address, ":") {
address = ":" + address
}
err := initConf(confPath)
if err != nil {
panic(err)
}
cache.InitActionsCommonCache()
plugins.InitDigestCache()
theme.InitTheme()
go cronClearCache()
}
func initConf(c string) (err error) {
err = config.InitConfig(c)
if err != nil {
return
}
err = config.InitTrans()
if err != nil {
return err
}
err = logs.InitLogger()
if err != nil {
return err
}
database, err := db.InitDb()
if err != nil {
return
}
model.InitDB(db.QueryDb(database))
err = wpconfig.InitOptions()
if err != nil {
return
}
err = wpconfig.InitTerms()
if err != nil {
return
}
wphandle.LoadPlugins()
return
}
func cronClearCache() {
t := time.NewTicker(config.GetConfig().CacheTime.CrontabClearCacheTime)
for {
select {
case <-t.C:
cachemanager.ClearExpired()
}
}
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
os.Exit(1)
}
}()
inits()
ossigns.SetConfPath(confPath)
go ossigns.SignalNotify()
Gin := route.SetupRouter()
c := config.GetConfig()
if c.Ssl.Key != "" && c.Ssl.Cert != "" {
err := Gin.RunTLS(address, c.Ssl.Cert, c.Ssl.Key)
if err != nil {
panic(err)
}
return
}
err := Gin.Run(address)
if err != nil {
panic(err)
}
}

View File

@ -1,69 +0,0 @@
package ossigns
import (
"fmt"
"github.com/fthvgb1/wp-go/app/mail"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/db"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/plugins/wphandle"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/signs"
"log"
"syscall"
)
var confPath string
func SetConfPath(path string) {
confPath = path
}
func FlushCache() {
defer func() {
if r := recover(); r != nil {
err := mail.SendMail([]string{config.GetConfig().Mail.User}, "清空缓存失败", fmt.Sprintf("err:[%s]", r))
logs.IfError(err, "发邮件失败")
}
}()
cachemanager.Flush()
log.Println("all cache flushed")
}
func Reloads() {
defer func() {
if r := recover(); r != nil {
log.Println(r)
}
}()
err := config.InitConfig(confPath)
logs.IfError(err, "获取配置文件失败", confPath)
err = logs.InitLogger()
logs.IfError(err, "日志配置错误")
_, err = db.InitDb()
logs.IfError(err, "重新读取db失败", config.GetConfig().Mysql)
err = wpconfig.InitOptions()
logs.IfError(err, "获取网站设置WpOption失败")
err = wpconfig.InitTerms()
logs.IfError(err, "获取WpTerms表失败")
wphandle.LoadPlugins()
reload.Reloads("themeArgAndConfig")
FlushCache()
log.Println("reload complete")
}
func SignalNotify() {
rel := func() bool {
go Reloads()
return true
}
flu := func() bool {
go FlushCache()
return true
}
signs.Install(syscall.SIGUSR1, rel, "reload")
signs.Install(syscall.SIGUSR2, flu, "flush")
signs.Wait()
}

126
app/pkg/cache/cache.go vendored
View File

@ -1,126 +0,0 @@
package cache
import (
"context"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/dao"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/cache/reload"
"time"
)
func InitActionsCommonCache() {
c := config.GetConfig()
cachemanager.NewMemoryMapCache(nil, dao.SearchPostIds, c.CacheTime.SearchPostCacheTime, "searchPostIds", func() time.Duration {
return config.GetConfig().CacheTime.SearchPostCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.SearchPostIds, c.CacheTime.PostListCacheTime, "listPostIds", func() time.Duration {
return config.GetConfig().CacheTime.PostListCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.MonthPost, c.CacheTime.MonthPostCacheTime, "monthPostIds", func() time.Duration {
return config.GetConfig().CacheTime.MonthPostCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.GetPostContext, c.CacheTime.ContextPostCacheTime, "postContext", func() time.Duration {
return config.GetConfig().CacheTime.ContextPostCacheTime
})
cachemanager.NewMemoryMapCache(dao.GetPostsByIds, nil, c.CacheTime.PostDataCacheTime, "postData", func() time.Duration {
return config.GetConfig().CacheTime.PostDataCacheTime
})
cachemanager.NewMemoryMapCache(dao.GetPostMetaByPostIds, nil, c.CacheTime.PostDataCacheTime, "postMetaData", func() time.Duration {
return config.GetConfig().CacheTime.PostDataCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.CategoriesAndTags, c.CacheTime.CategoryCacheTime, "categoryAndTagsData", func() time.Duration {
return config.GetConfig().CacheTime.CategoryCacheTime
})
cachemanager.NewVarMemoryCache(dao.RecentPosts, c.CacheTime.RecentPostCacheTime, "recentPosts", func() time.Duration {
return config.GetConfig().CacheTime.RecentPostCacheTime
})
cachemanager.NewVarMemoryCache(RecentComment, c.CacheTime.RecentCommentsCacheTime, "recentComments", func() time.Duration {
return config.GetConfig().CacheTime.RecentCommentsCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.CommentNum, 30*time.Second, "commentNumber", func() time.Duration {
return config.GetConfig().CacheTime.CommentsIncreaseUpdateTime
})
cachemanager.NewMemoryMapCache(nil, PostTopComments, 30*time.Second, "PostCommentsIds", func() time.Duration {
return config.GetConfig().CacheTime.CommentsIncreaseUpdateTime
})
cachemanager.NewMemoryMapCache(nil, dao.PostTopCommentNum, 30*time.Second, "postTopCommentsNum", func() time.Duration {
return config.GetConfig().CacheTime.CommentsIncreaseUpdateTime
})
cachemanager.NewMemoryMapCache(dao.GetCommentByIds, nil, time.Hour, "postCommentData", func() time.Duration {
return config.GetConfig().CacheTime.CommentsCacheTime
})
cachemanager.NewMemoryMapCache(dao.CommentChildren, nil, time.Minute, "commentChildren", func() time.Duration {
return config.GetConfig().CacheTime.CommentsIncreaseUpdateTime
})
cachemanager.NewVarMemoryCache(dao.GetMaxPostId, c.CacheTime.MaxPostIdCacheTime, "maxPostId", func() time.Duration {
return config.GetConfig().CacheTime.MaxPostIdCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.GetUserById, c.CacheTime.UserInfoCacheTime, "userData", func() time.Duration {
return config.GetConfig().CacheTime.UserInfoCacheTime
})
cachemanager.NewMemoryMapCache(nil, dao.GetUserByName, c.CacheTime.UserInfoCacheTime, "usernameToUserData", func() time.Duration {
return config.GetConfig().CacheTime.UserInfoCacheTime
})
cachemanager.NewVarMemoryCache(dao.AllUsername, c.CacheTime.UserInfoCacheTime, "allUsername", func() time.Duration {
return config.GetConfig().CacheTime.UserInfoCacheTime
})
cachemanager.NewVarMemoryCache(SiteFeed, time.Hour, "siteFeed")
cachemanager.NewMemoryMapCache(nil, PostFeed, time.Hour, "postFeed")
cachemanager.NewVarMemoryCache(CommentsFeed, time.Hour, "commentsFeed")
cachemanager.NewMemoryMapCache[string, string](nil, nil, 15*time.Minute, "NewComment")
InitFeed()
}
type Arch struct {
data []models.PostArchive
fn func(context.Context) ([]models.PostArchive, error)
month time.Month
}
var arch = reload.Vars(Arch{
fn: dao.Archives,
}, "archives-year-month-data")
func Archives(ctx context.Context) []models.PostArchive {
a := arch.Load()
data := a.data
l := len(data)
m := time.Now().Month()
if l < 1 || a.month != m {
r, err := a.fn(ctx)
if err != nil {
logs.Error(err, "set cache Archives fail")
return nil
}
a.month = m
a.data = r
arch.Store(a)
data = r
}
return data
}

View File

@ -1,39 +0,0 @@
package cache
import (
"context"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/helper/slice"
"time"
)
// CategoriesTags get all categories or tags
//
// query func see dao.CategoriesAndTags
//
// t is constraints.Tag or constraints.Category
func CategoriesTags(ctx context.Context, t ...string) []models.TermsMy {
tt := ""
if len(t) > 0 {
tt = t[0]
}
r, err := cachemanager.GetBy[[]models.TermsMy]("categoryAndTagsData", ctx, tt, time.Second)
logs.IfError(err, "get category fail")
return r
}
func AllCategoryTagsNames(ctx context.Context, t ...string) map[string]struct{} {
tt := ""
if len(t) > 0 {
tt = t[0]
}
r, err := cachemanager.GetBy[[]models.TermsMy]("categoryAndTagsData", ctx, tt, time.Second)
if err != nil {
logs.Error(err, "get category fail")
return nil
}
return slice.ToMap(r, func(t models.TermsMy) (string, struct{}) {
return t.Name, struct{}{}
}, true)
}

View File

@ -1,124 +0,0 @@
package cache
import (
"context"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/dao"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/number"
str "github.com/fthvgb1/wp-go/helper/strings"
"time"
)
// RecentComments query func see RecentComment
func RecentComments(ctx context.Context, n int) (r []models.Comments) {
nn := number.Max(n, 10)
r, err := cachemanager.GetVarVal[[]models.Comments]("recentComments", ctx, time.Second, ctx, nn)
if len(r) > n {
r = r[0:n]
}
logs.IfError(err, "get recent comment fail")
return
}
// PostTopLevelCommentIds query func see PostTopComments
func PostTopLevelCommentIds(ctx context.Context, postId uint64, page, limit, total int, order string, a ...any) ([]uint64, error) {
var key string
if len(a) > 0 {
key = helper.ParseArgs("", a...)
}
if key == "" {
key = fmt.Sprintf("%d-%d-%d-%d-%s", postId, page, limit, total, order)
}
return cachemanager.GetBy[[]uint64]("PostCommentsIds", ctx,
key, time.Second, postId, page, limit, 0, order)
}
// GetCommentById query func see dao.GetCommentByIds
func GetCommentById(ctx context.Context, id uint64) (models.Comments, error) {
return cachemanager.GetBy[models.Comments]("postCommentData", ctx, id, time.Second)
}
// GetCommentDataByIds query func see dao.GetCommentByIds
func GetCommentDataByIds(ctx context.Context, ids []uint64) ([]models.Comments, error) {
return cachemanager.GetBatchBy[models.Comments]("postCommentData", ctx, ids, time.Second)
}
func NewCommentCache() *cache.MapCache[string, string] {
r, _ := cachemanager.GetMapCache[string, string]("NewComment")
return r
}
func PostTopComments(ctx context.Context, _ string, a ...any) ([]uint64, error) {
postId := a[0].(uint64)
page := a[1].(int)
limit := a[2].(int)
total := a[3].(int)
order := helper.ParseArgs("", a...)
if order == "" {
order = wpconfig.GetOption("comment_order")
}
v, _, err := dao.PostCommentsIds(ctx, postId, page, limit, total, order)
if err != nil {
return nil, err
}
return v, nil
}
func RecentComment(ctx context.Context, a ...any) (r []models.Comments, err error) {
r, err = dao.RecentComments(ctx, a...)
if err != nil {
return r, err
}
for i, comment := range r {
r[i].CommentAuthorUrl, err = GetCommentUrl(ctx, comment.CommentId, comment.CommentPostId)
if err != nil {
return nil, err
}
}
return
}
func GetCommentUrl(ctx context.Context, commentId, postId uint64) (string, error) {
if wpconfig.GetOption("page_comments") != "1" {
return fmt.Sprintf("/p/%d#comment-%d", postId, commentId), nil
}
commentsPerPage := str.ToInteger(wpconfig.GetOption("comments_per_page"), 5)
topCommentId, err := AncestorCommentId(ctx, commentId)
if err != nil {
return "", err
}
totalNum, err := cachemanager.GetBy[int]("postTopCommentsNum", ctx, postId, time.Second)
if err != nil {
return "", err
}
if totalNum <= commentsPerPage {
return fmt.Sprintf("/p/%d#comment-%d", postId, commentId), nil
}
num, err := dao.PreviousCommentNum(ctx, topCommentId, postId)
if err != nil {
return "", err
}
order := wpconfig.GetOption("comment_order")
page := number.DivideCeil(num+1, commentsPerPage)
if order == "desc" {
page = number.DivideCeil(totalNum-num, commentsPerPage)
}
return fmt.Sprintf("/p/%d/comment-page-%d/#comment-%d", postId, page, commentId), nil
}
func AncestorCommentId(ctx context.Context, commentId uint64) (uint64, error) {
comment, err := GetCommentById(ctx, commentId)
if err != nil {
return 0, err
}
if comment.CommentParent == 0 {
return comment.CommentId, nil
}
return AncestorCommentId(ctx, comment.CommentParent)
}

View File

@ -1,17 +0,0 @@
package cache
import (
"context"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"time"
)
// GetPostMetaByPostIds query func see dao.GetPostMetaByPostIds
func GetPostMetaByPostIds(ctx context.Context, ids []uint64) ([]map[string]any, error) {
return cachemanager.GetBatchBy[map[string]any]("postMetaData", ctx, ids, time.Second)
}
// GetPostMetaByPostId query func see dao.GetPostMetaByPostIds
func GetPostMetaByPostId(ctx context.Context, id uint64) (map[string]any, error) {
return cachemanager.GetBy[map[string]any]("postMetaData", ctx, id, time.Second)
}

View File

@ -1,91 +0,0 @@
package cache
import (
"context"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/dao"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/helper/number"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"time"
)
// GetPostById query func see dao.GetPostsByIds
func GetPostById(ctx context.Context, id uint64) (models.Posts, error) {
return cachemanager.GetBy[models.Posts]("postData", ctx, id, time.Second)
}
// GetPostsByIds query func see dao.GetPostsByIds
func GetPostsByIds(ctx context.Context, ids []uint64) ([]models.Posts, error) {
return cachemanager.GetBatchBy[models.Posts]("postData", ctx, ids, time.Second)
}
// SearchPost query func see dao.SearchPostIds
func SearchPost(ctx context.Context, key string, args ...any) (r []models.Posts, total int, err error) {
ids, err := cachemanager.GetBy[dao.PostIds]("searchPostIds", ctx, key, time.Second, args...)
if err != nil {
return
}
total = ids.Length
r, err = GetPostsByIds(ctx, ids.Ids)
return
}
// PostLists query func see dao.SearchPostIds
func PostLists(ctx context.Context, key string, args ...any) (r []models.Posts, total int, err error) {
ids, err := cachemanager.GetBy[dao.PostIds]("listPostIds", ctx, key, time.Second, args...)
if err != nil {
return
}
total = ids.Length
r, err = GetPostsByIds(ctx, ids.Ids)
return
}
// GetMaxPostId query func see dao.GetMaxPostId
func GetMaxPostId(ctx context.Context) (uint64, error) {
return cachemanager.GetVarVal[uint64]("maxPostId", ctx, time.Second)
}
// RecentPosts query func see dao.RecentPosts
func RecentPosts(ctx context.Context, n int) (r []models.Posts) {
nn := n
feedNum := str.ToInteger(wpconfig.GetOption("posts_per_rss"), 10)
nn = number.Max(n, feedNum)
r, err := cachemanager.GetVarVal[[]models.Posts]("recentPosts", ctx, time.Second, nn)
if n < len(r) {
r = r[:n]
}
logs.IfError(err, "get recent post")
return
}
// GetContextPost query func see dao.GetPostContext
func GetContextPost(ctx context.Context, id uint64, date time.Time) (prev, next models.Posts, err error) {
postCtx, err := cachemanager.GetBy[dao.PostContext]("postContext", ctx, id, time.Second, date)
if err != nil {
return models.Posts{}, models.Posts{}, err
}
prev = postCtx.Prev
next = postCtx.Next
return
}
// GetMonthPostIds query func see dao.MonthPost
func GetMonthPostIds(ctx context.Context, year, month string, page, limit int, order string) (r []models.Posts, total int, err error) {
res, err := cachemanager.GetBy[[]uint64]("monthPostIds", ctx, fmt.Sprintf("%s%s", year, month), time.Second, year, month)
if err != nil {
return
}
if order == "desc" {
res = slice.Reverse(res)
}
total = len(res)
rr := slice.Pagination(res, page, limit)
r, err = GetPostsByIds(ctx, rr)
return
}

View File

@ -1,26 +0,0 @@
package cache
import (
"context"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"time"
)
// GetUserByName query func see dao.GetUserByName
func GetUserByName(ctx context.Context, username string) (models.Users, error) {
return cachemanager.GetBy[models.Users]("usernameToUserData", ctx, username, time.Second)
}
// GetAllUsername query func see dao.AllUsername
func GetAllUsername(ctx context.Context) (map[string]uint64, error) {
return cachemanager.GetVarVal[map[string]uint64]("allUsername", ctx, time.Second)
}
// GetUserById query func see dao.GetUserById
func GetUserById(ctx context.Context, uid uint64) models.Users {
r, err := cachemanager.GetBy[models.Users]("userData", ctx, uid, time.Second)
logs.IfError(err, "get user", uid)
return r
}

View File

@ -1,147 +0,0 @@
package config
import (
"fmt"
"github.com/fthvgb1/wp-go/safety"
"gopkg.in/yaml.v2"
"io"
"net/http"
"os"
"strings"
"time"
)
var config safety.Var[Config]
func GetConfig() Config {
return config.Load()
}
type Config struct {
Ssl Ssl `yaml:"ssl" json:"ssl"`
Mysql Mysql `yaml:"mysql" json:"mysql"`
Mail Mail `yaml:"mail" json:"mail"`
CacheTime CacheTime `yaml:"cacheTime" json:"cacheTime"`
PluginPath string `yaml:"pluginPath" json:"pluginPath"`
ExternScript []string `json:"externScript" yaml:"externScript"`
DigestWordCount int `yaml:"digestWordCount" json:"digestWordCount,omitempty"`
DigestAllowTag string `yaml:"digestAllowTag" json:"digestAllowTag"`
MaxRequestSleepNum int64 `yaml:"maxRequestSleepNum" json:"maxRequestSleepNum,omitempty"`
MaxRequestNum int64 `yaml:"maxRequestNum" json:"maxRequestNum,omitempty"`
SingleIpSearchNum int64 `yaml:"singleIpSearchNum" json:"singleIpSearchNum,omitempty"`
Gzip bool `yaml:"gzip" json:"gzip,omitempty"`
PostCommentUrl string `yaml:"postCommentUrl" json:"postCommentUrl,omitempty"`
TrustIps []string `yaml:"trustIps" json:"trustIps,omitempty"`
TrustServerNames []string `yaml:"trustServerNames" json:"trustServerNames,omitempty"`
Theme string `yaml:"theme" json:"theme,omitempty"`
PostOrder string `yaml:"postOrder" json:"postOrder,omitempty"`
UploadDir string `yaml:"uploadDir" json:"uploadDir,omitempty"`
Pprof string `yaml:"pprof" json:"pprof,omitempty"`
ListPagePlugins []string `yaml:"listPagePlugins" json:"listPagePlugins,omitempty"`
PaginationStep int `yaml:"paginationStep" json:"paginationStep,omitempty"`
ShowQuerySql bool `yaml:"showQuerySql" json:"showQuerySql,omitempty"`
Plugins []string `yaml:"plugins" json:"plugins,omitempty"`
LogOutput string `yaml:"logOutput" json:"logOutput,omitempty"`
WpDir string `yaml:"wpDir" json:"wpDir"`
}
type CacheTime struct {
CacheControl time.Duration `yaml:"cacheControl" json:"cacheControl,omitempty"`
RecentPostCacheTime time.Duration `yaml:"recentPostCacheTime" json:"recentPostCacheTime,omitempty"`
CategoryCacheTime time.Duration `yaml:"categoryCacheTime" json:"categoryCacheTime,omitempty"`
ArchiveCacheTime time.Duration `yaml:"archiveCacheTime" json:"archiveCacheTime,omitempty"`
ContextPostCacheTime time.Duration `yaml:"contextPostCacheTime" json:"contextPostCacheTime,omitempty"`
RecentCommentsCacheTime time.Duration `yaml:"recentCommentsCacheTime" json:"recentCommentsCacheTime,omitempty"`
DigestCacheTime time.Duration `yaml:"digestCacheTime" json:"digestCacheTime,omitempty"`
PostListCacheTime time.Duration `yaml:"postListCacheTime" json:"postListCacheTime,omitempty"`
SearchPostCacheTime time.Duration `yaml:"searchPostCacheTime" json:"searchPostCacheTime,omitempty"`
MonthPostCacheTime time.Duration `yaml:"monthPostCacheTime" json:"monthPostCacheTime,omitempty"`
PostDataCacheTime time.Duration `yaml:"postDataCacheTime" json:"postDataCacheTime,omitempty"`
PostCommentsCacheTime time.Duration `yaml:"postCommentsCacheTime" json:"postCommentsCacheTime,omitempty"`
CrontabClearCacheTime time.Duration `yaml:"crontabClearCacheTime" json:"crontabClearCacheTime,omitempty"`
MaxPostIdCacheTime time.Duration `yaml:"maxPostIdCacheTime" json:"maxPostIdCacheTime,omitempty"`
UserInfoCacheTime time.Duration `yaml:"userInfoCacheTime" json:"userInfoCacheTime,omitempty"`
CommentsCacheTime time.Duration `yaml:"commentsCacheTime" json:"commentsCacheTime,omitempty"`
SleepTime []time.Duration `yaml:"sleepTime" json:"sleepTime,omitempty"`
CommentsIncreaseUpdateTime time.Duration `yaml:"commentsIncreaseUpdateTime" json:"commentsIncreaseUpdateTime"`
}
type Ssl struct {
Cert string `yaml:"cert" json:"cert,omitempty"`
Key string `yaml:"key" json:"key,omitempty"`
}
type Mail struct {
User string `yaml:"user" json:"user,omitempty"`
Alias string `yaml:"alias" json:"alias,omitempty"`
Pass string `yaml:"pass" json:"pass,omitempty"`
Host string `yaml:"host" json:"host,omitempty"`
Port int `yaml:"port" json:"port,omitempty"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify" json:"insecureSkipVerify,omitempty"`
}
type Mysql struct {
Dsn Dsn `yaml:"dsn" json:"dsn"`
Pool Pool `yaml:"pool" json:"pool"`
}
func GetCustomizedConfig[T any]() (T, error) {
var r T
err := yaml.Unmarshal(fileData.Load(), &r)
return r, err
}
var fileData = safety.NewVar([]byte{})
func InitConfig(conf string) error {
if conf == "" {
conf = "config.yaml"
}
var file []byte
var err error
if strings.Contains(conf, "http") {
get, err := http.Get(conf)
if err != nil {
return err
}
defer get.Body.Close()
file, err = io.ReadAll(get.Body)
} else {
file, err = os.ReadFile(conf)
}
if err != nil {
return err
}
fileData.Store(file)
var c Config
err = yaml.Unmarshal(file, &c)
if err != nil {
return err
}
config.Store(c)
return nil
}
type Dsn struct {
Host string `yaml:"host" json:"host,omitempty"`
Port string `yaml:"port" json:"port,omitempty"`
Db string `yaml:"db" json:"db,omitempty"`
User string `yaml:"user" json:"user,omitempty"`
Password string `yaml:"password" json:"password,omitempty"`
Charset string `yaml:"charset" json:"charset,omitempty"`
}
func (m Dsn) GetDsn() string {
if m.Charset == "" {
m.Charset = "utf8"
}
t := "%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local"
return fmt.Sprintf(t, m.User, m.Password, m.Host, m.Port, m.Db, m.Charset)
}
type Pool struct {
ConnMaxIdleTime time.Duration `yaml:"connMaxIdleTime" json:"connMaxIdleTime,omitempty"`
MaxOpenConn int `yaml:"maxOpenConn" json:"maxOpenConn,omitempty"`
MaxIdleConn int `yaml:"maxIdleConn" json:"maxIdleConn,omitempty"`
ConnMaxLifetime time.Duration `yaml:"connMaxLifetime" json:"connMaxLifetime,omitempty"`
}

View File

@ -1,43 +0,0 @@
package config
import (
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTrans "github.com/go-playground/validator/v10/translations/en"
zhTrans "github.com/go-playground/validator/v10/translations/zh"
"reflect"
)
var enT ut.Translator
var zhT ut.Translator
func GetZh() ut.Translator {
return zhT
}
func GetEn() ut.Translator {
return enT
}
func InitTrans() error {
if validate, ok := binding.Validator.Engine().(*validator.Validate); ok {
ens := en.New()
uni := ut.New(ens, zh.New(), ens)
zhT, _ = uni.GetTranslator("zh")
enT, _ = uni.GetTranslator("en")
err := enTrans.RegisterDefaultTranslations(validate, enT)
if err != nil {
return err
}
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
return field.Tag.Get("label")
})
err = zhTrans.RegisterDefaultTranslations(validate, zhT)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,10 +0,0 @@
package blocks
const (
Search = "block-search"
RecentPosts = "block-recent-posts"
RecentComments = "block-recent-comments"
Archive = "block-archives"
Categories = "block-categories"
Meta = "block-meta"
)

View File

@ -1,30 +0,0 @@
package constraints
const (
Home = "Home"
Archive = "Archive"
Category = "Category"
Tag = "Tag"
Search = "Search"
Author = "Author"
Detail = "Detail"
NoRoute = "NoRoute"
Ok = "Ok"
Error404 = "Error404"
ParamError = "ParamError"
InternalErr = "InternalErr"
AllStats = "AllStats"
AllScene = "AllScene"
PipeData = "PipeData"
PipeMiddleware = "PipeMiddleware"
PipeRender = "PipeRender"
Defaults = "default"
HeadScript = "headScript"
FooterScript = "footerScript"
SidebarsWidgets = "sidebarsWidgets"
)

View File

@ -1,12 +0,0 @@
package widgets
const (
Search = "widget-search"
RecentPosts = "widget-recent-posts"
RecentComments = "widget-recent-comments"
Archive = "widget-archives"
Categories = "widget-categories"
Meta = "widget-meta"
Widget = "widget"
)

View File

@ -1,183 +0,0 @@
package dao
import (
"context"
"database/sql"
"errors"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/number"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/model"
)
// RecentComments
// param context.Context
func RecentComments(ctx context.Context, a ...any) (r []models.Comments, err error) {
n := helper.ParseArgs(10, a...)
return model.Finds[models.Comments](ctx, model.Conditions(
model.Where(model.SqlBuilder{
{"comment_approved", "1"},
{"post_status", "publish"},
}),
model.Fields("comment_ID,comment_author,comment_post_ID,post_title"),
model.Order(model.SqlBuilder{{"comment_date_gmt", "desc"}}),
model.Join(model.SqlBuilder{{"a", "left join", "wp_posts b", "a.comment_post_ID=b.ID"}}),
model.Limit(n),
))
}
// PostComments
// param1 context.Context
// param2 postId
func PostComments(ctx context.Context, postId uint64, _ ...any) ([]uint64, error) {
r, err := model.ChunkFind[models.Comments](ctx, 300, model.Conditions(
model.Where(model.SqlBuilder{
{"comment_approved", "1"},
{"comment_post_ID", "=", number.IntToString(postId), "int"},
}),
model.Fields("comment_ID"),
model.Order(model.SqlBuilder{
{"comment_date_gmt", "asc"},
{"comment_ID", "asc"},
})),
)
if err != nil {
return nil, err
}
return slice.Map(r, func(t models.Comments) uint64 {
return t.CommentId
}), err
}
func GetCommentByIds(ctx context.Context, ids []uint64, _ ...any) (map[uint64]models.Comments, error) {
if len(ids) < 1 {
return nil, nil
}
m := make(map[uint64]models.Comments)
off := 0
for {
id := slice.Slice(ids, off, 500)
if len(id) < 1 {
break
}
r, err := model.Finds[models.Comments](ctx, model.Conditions(
model.Where(model.SqlBuilder{
{"comment_ID", "in", ""}, {"comment_approved", "1"},
}),
model.Fields("*"),
model.In(slice.ToAnySlice(id)),
))
if err != nil {
return m, err
}
for _, comments := range r {
m[comments.CommentId] = comments
}
off += 500
}
return m, nil
}
func CommentNum(ctx context.Context, postId uint64, _ ...any) (int, error) {
n, err := model.GetField[models.Posts](ctx, "comment_count", model.Conditions(
model.Where(model.SqlBuilder{{"ID", "=", number.IntToString(postId), "int"}})))
if err != nil {
return 0, err
}
return str.ToInteger(n, 0), err
}
func PostTopCommentNum(ctx context.Context, postId uint64, _ ...any) (int, error) {
v, err := model.GetField[models.Comments](ctx, "count(*) num", model.Conditions(
model.Where(postTopCommentNumWhere(postId)),
))
if err != nil {
return 0, err
}
return str.ToInteger(v, 0), nil
}
func postTopCommentNumWhere(postId uint64) model.SqlBuilder {
threadComments := wpconfig.GetOption("thread_comments")
pageComments := wpconfig.GetOption("page_comments")
where := model.SqlBuilder{
{"comment_approved", "1"},
{"comment_post_ID", "=", number.IntToString(postId), "int"},
}
if pageComments != "1" || threadComments == "1" || "1" == wpconfig.GetOption("thread_comments_depth") {
where = append(where, []string{"comment_parent", "0"})
}
return where
}
func PostCommentsIds(ctx context.Context, postId uint64, page, limit, totalRaw int, order string) ([]uint64, int, error) {
condition := model.Conditions(
model.Where(postTopCommentNumWhere(postId)),
model.TotalRaw(totalRaw),
model.Fields("comment_ID"),
model.Order(model.SqlBuilder{
{"comment_date_gmt", order},
{"comment_ID", "asc"},
}),
)
var r []models.Comments
var total int
var err error
if limit < 1 {
r, err = model.ChunkFind[models.Comments](ctx, 300, condition)
total = len(r)
} else {
r, total, err = model.Pagination[models.Comments](ctx, condition, page, limit)
}
if err != nil && errors.Is(err, sql.ErrNoRows) {
err = nil
}
return slice.Map(r, func(t models.Comments) uint64 {
return t.CommentId
}), total, err
}
func CommentChildren(ctx context.Context, commentIds []uint64, _ ...any) (r map[uint64][]uint64, err error) {
rr, err := model.Finds[models.Comments](ctx, model.Conditions(
model.Where(model.SqlBuilder{
{"comment_parent", "in", ""},
{"comment_approved", "1"},
}),
model.In(slice.ToAnySlice(commentIds)),
model.Fields("comment_ID,comment_parent"),
))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
rrr := slice.GroupBy(rr, func(v models.Comments) (uint64, uint64) {
return v.CommentParent, v.CommentId
})
r = make(map[uint64][]uint64)
for _, id := range commentIds {
r[id] = rrr[id]
}
return
}
func PreviousCommentNum(ctx context.Context, commentId, postId uint64) (int, error) {
v, err := model.GetField[models.Comments](ctx, "count(*)", model.Conditions(
model.Where(model.SqlBuilder{
{"comment_approved", "1"},
{"comment_post_ID", "=", number.IntToString(postId), "int"},
{"comment_ID", "<", number.IntToString(commentId), "int"},
{"comment_parent", "=", "0", "int"},
}),
))
if err != nil {
return 0, err
}
return str.ToInteger(v, 0), nil
}

View File

@ -1,32 +0,0 @@
package dao
import (
"context"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/helper/slice"
"github.com/fthvgb1/wp-go/model"
)
func GetUserById(ctx context.Context, uid uint64, _ ...any) (r models.Users, err error) {
r, err = model.FindOneById[models.Users](ctx, uid)
return
}
func AllUsername(ctx context.Context, _ ...any) (map[string]uint64, error) {
r, err := model.SimpleFind[models.Users](ctx, model.SqlBuilder{
{"user_status", "=", "0", "int"},
}, "display_name,ID")
if err != nil {
return nil, err
}
return slice.ToMap(r, func(t models.Users) (string, uint64) {
return t.DisplayName, t.Id
}, true), nil
}
func GetUserByName(ctx context.Context, u string, _ ...any) (r models.Users, err error) {
r, err = model.FirstOne[models.Users](ctx, model.SqlBuilder{{
"display_name", u,
}}, "*", nil)
return
}

View File

@ -1,76 +0,0 @@
package logs
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/safety"
"io"
"log"
"os"
"runtime"
"strings"
)
var logs = safety.NewVar[*log.Logger](nil)
var logFile = safety.NewVar[*os.File](nil)
func InitLogger() error {
c := config.GetConfig()
return SetLogger(c.LogOutput)
}
func SetLogger(loggerFile string) error {
if loggerFile == "" {
loggerFile = "stderr"
}
preFD := logFile.Load()
l := &log.Logger{}
var out io.Writer
switch loggerFile {
case "stdout":
out = os.Stdout
case "stderr":
out = os.Stderr
default:
file, err := os.OpenFile(loggerFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0777)
if err != nil {
return err
}
out = file
logFile.Store(file)
}
logs.Store(l)
if preFD != nil {
_ = preFD.Close()
}
l.SetFlags(log.Ldate | log.Ltime)
l.SetOutput(out)
return nil
}
func Errs(err error, depth int, desc string, args ...any) {
var pcs [1]uintptr
runtime.Callers(depth, pcs[:])
f := runtime.CallersFrames([]uintptr{pcs[0]})
ff, _ := f.Next()
s := strings.Builder{}
_, _ = fmt.Fprintf(&s, "%s:%d %s err:[%s]", ff.File, ff.Line, desc, err)
if len(args) > 0 {
s.WriteString(" args:")
for _, arg := range args {
_, _ = fmt.Fprintf(&s, "%v", arg)
}
}
logs.Load().Println(s.String())
}
func Error(err error, desc string, args ...any) {
Errs(err, 3, desc, args...)
}
func IfError(err error, desc string, args ...any) {
if err == nil {
return
}
Errs(err, 3, desc, args...)
}

View File

@ -1,19 +0,0 @@
package relation
import (
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/model"
)
var PostsWithAuthor = model.RelationHasOne(func(m *models.Posts) uint64 {
return m.PostAuthor
}, func(p *models.Users) uint64 {
return p.Id
}, func(m *models.Posts, p *models.Users) {
m.Author = p
}, model.Relationship{
RelationType: model.HasOne,
Table: "wp_users user",
ForeignKey: "ID",
Local: "post_author",
})

View File

@ -1,9 +0,0 @@
<div>
{{.aa}}
</div>
<div>
{{ range $k,$v := .posts}}
<h2>{{$v.PostTitle}} </h2>
{{end}}
</div>

View File

@ -1,81 +0,0 @@
package main
import (
"embed"
"github.com/fthvgb1/wp-go/app/cmd/route"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/plugins/wphandle"
"github.com/fthvgb1/wp-go/app/theme"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components"
"github.com/fthvgb1/wp-go/app/theme/wp/components/widget"
route2 "github.com/fthvgb1/wp-go/app/theme/wp/route"
"github.com/gin-gonic/gin"
"html/template"
"net/http"
"plugintt/xx"
)
//go:embed a.gohtml
var em embed.FS
var tt *template.Template
func init() {
// register as theme
theme.AddThemeHookFunc("themename", hook)
//use the local template
//note: must use embed.FS
t, err := template.ParseFS(em, "a.gohtml")
if err != nil {
logs.Error(err, "")
}
tt = t
// register gin route. it will be effecting when server restart.
route.Hook(func(r *gin.Engine) {
r.GET("xx", func(c *gin.Context) {
c.String(http.StatusOK, "xxoo")
})
})
}
func hook(h *wp.Handle) {
wp.Run(h, config)
}
func config(h *wp.Handle) {
// same theme config
wphandle.UsePlugins(h)
wp.InitPipe(h)
h.PushHandler(constraints.PipeMiddleware, constraints.Home,
wp.NewHandleFn(widget.IsCategory, 100, "widget.IsCategory"))
components.WidgetArea(h)
h.PushHandler(constraints.PipeRender, constraints.Home, wp.NewHandleFn(func(h *wp.Handle) {
h.SetData("aa", "xyxxxx")
h.RenderHtml(tt, http.StatusOK, "a.gohtml")
h.Abort()
h.StopPipe()
}, 10, "renderHome"))
// use simple reg route
route2.PushRoute(`(?P<control>\w+)/(?P<method>\w+)`, route2.Route{
Path: `(?P<control>\w+)/(?P<method>\w+)`,
Scene: constraints.Home,
Method: []string{"GET"},
Type: "reg",
})
//...
}
// Xo to be a func when theme init
func Xo(h *wp.Handle) {
xx.Xo()
route2.Delete(`(?P<control>\w+)/(?P<method>\w+)`)
h.ReplaceHandle(constraints.PipeRender, "wp.RenderTemplate", func(h *wp.Handle) {
h.SetData("aa", "xyxxxx")
h.RenderHtml(tt, http.StatusOK, "a.gohtml")
h.StopPipe()
})
}

View File

@ -1,7 +0,0 @@
#/bin/bash
# copy plugintt to other dir and remove .dev suffix
# note the go version and build tool flag must same to server build
# eg: -gcflags all="-N -l" --race may used in ide debug
go mod tidy
go build -buildmode=plugin -o xx.so main.go

View File

@ -1,141 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/dao"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/number"
"github.com/fthvgb1/wp-go/helper/slice"
"github.com/fthvgb1/wp-go/helper/strings"
"github.com/redis/go-redis/v9"
"strconv"
str "strings"
"time"
)
type RdmCache[K comparable, V any] struct {
expired func() time.Duration
rdb *redis.Client
keyFn func(K) string
name string
resFn func(map[string]string) V
saveData func(V) map[string]string
}
func (r *RdmCache[K, V]) SetExpiredTime(f func() time.Duration) {
r.expired = f
}
func (r *RdmCache[K, V]) Get(ctx context.Context, key K) (V, bool) {
var re V
result, err := r.rdb.Exists(ctx, r.keyFn(key)).Result()
if result <= 0 || err != nil {
return re, false
}
rr, err := r.rdb.HGetAll(ctx, r.keyFn(key)).Result()
if errors.Is(err, redis.Nil) {
return re, false
}
if err != nil {
return re, false
}
return r.resFn(rr), true
}
func (r *RdmCache[K, V]) Set(ctx context.Context, key K, val V) {
k := r.keyFn(key)
result, err := r.rdb.HSet(ctx, k, r.saveData(val)).Result()
b, err := r.rdb.Expire(ctx, k, r.expired()).Result()
if err != nil {
fmt.Println(result, b, err)
return
}
fmt.Println(result, err)
}
func (r *RdmCache[K, V]) GetExpireTime(ctx context.Context) time.Duration {
return r.expired()
}
func (r *RdmCache[K, V]) Ttl(ctx context.Context, key K) time.Duration {
result, err := r.rdb.TTL(ctx, r.keyFn(key)).Result()
if err != nil {
return 0
}
return result
}
func (r *RdmCache[K, V]) Flush(ctx context.Context) {
fmt.Println("flush redis cache")
}
func (r *RdmCache[K, V]) Del(ctx context.Context, key ...K) {
r.rdb.Del(ctx, slice.Map(key, r.keyFn)...)
}
func (r *RdmCache[K, V]) ClearExpired(ctx context.Context) {
fmt.Println("clear expired redis cache")
}
// RedisCache use step:
// 1 go build -gcflags all="-N -l" --race -buildmode=plugin -o redisCache.so main.go && cp ./redisCache.so ../wp-go/plugins/
// 2 wp-go config add redisCache plugin
func RedisCache(h *wp.Handle) {
vv, ok := cachemanager.GetMapCache[string, dao.PostIds]("listPostIds")
if ok {
_, ok := any(vv.Cache).(*RdmCache[string, dao.PostIds])
if ok {
return
}
}
reload.AppendOnceFn(func() {
err := cachemanager.SetMapCache("listPostIds", vv)
if err != nil {
logs.Error(err, "set recovery listPostIds cache err")
} else {
cachemanager.PushOrSetFlush(cachemanager.Queue{Name: "listPostIds", Fn: vv.Flush})
cachemanager.PushOrSetClearExpired(cachemanager.Queue{Name: "listPostIds", Fn: vv.Flush})
fmt.Println("recovery listPostIds cache ok")
}
})
rdm := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
r := RdmCache[string, dao.PostIds]{
expired: func() time.Duration {
return time.Minute
},
keyFn: func(u string) string {
return strings.Join("postIds:", u)
},
rdb: rdm,
name: "",
resFn: func(m map[string]string) dao.PostIds {
return dao.PostIds{
Ids: slice.Map(str.Split(m["ids"], ","), strings.ToInt[uint64]),
Length: strings.ToInt[int](m["length"]),
}
},
saveData: func(ids dao.PostIds) map[string]string {
t := slice.Map(ids.Ids, number.IntToString[uint64])
return map[string]string{
"ids": str.Join(t, ","),
"length": strconv.Itoa(ids.Length),
}
},
}
cachemanager.NewMapCache[string, dao.PostIds](&r, nil, dao.SearchPostIds, config.GetConfig().CacheTime.PostListCacheTime, "listPostIds", func() time.Duration {
return config.GetConfig().CacheTime.PostListCacheTime
})
fmt.Println("redis cache inited ok")
}

View File

@ -1,50 +0,0 @@
module redisCache
go 1.21
require (
github.com/fthvgb1/wp-go v0.0.0-20231210111549-d72bed0c8c4e
github.com/redis/go-redis/v9 v9.3.0
)
replace github.com/fthvgb1/wp-go v0.0.0-20231210111549-d72bed0c8c4e => ../wp-go
require (
github.com/bytedance/sonic v1.10.2 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/elliotchance/phpserialize v1.3.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sessions v0.0.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.16.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -1,11 +0,0 @@
package xx
import (
"fmt"
"github.com/shopspring/decimal"
)
func Xo() {
fmt.Println("xxoo")
fmt.Println(decimal.Max(decimal.NewFromFloat(32.3333333333333), decimal.NewFromFloat(32.33333333333331)).String())
}

View File

@ -1,187 +0,0 @@
package plugins
import (
"context"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/maps"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/plugin/digest"
"github.com/fthvgb1/wp-go/safety"
"regexp"
"strings"
"time"
"unicode/utf8"
)
var more = regexp.MustCompile("<!--more(.*?)?-->")
var removeWpBlock = regexp.MustCompile("<!-- /?wp:.*-->")
type DigestConfig struct {
DigestWordCount int `yaml:"digestWordCount"`
DigestAllowTag string `yaml:"digestAllowTag"`
DigestRegex string `yaml:"digestRegex"`
DigestTagOccupyNum []struct {
Tag string `yaml:"tag"`
Num int `yaml:"num"`
ChuckOvered bool `yaml:"chuckOvered"`
EscapeCharacter []struct {
Tags string `yaml:"tags"`
Character []string `yaml:"character"`
Num int `yaml:"num"`
ChuckOvered bool `yaml:"chuckOvered"`
} `yaml:"escapeCharacter"`
} `yaml:"digestTagOccupyNum"`
specialSolve map[string]digest.SpecialSolveConf
}
var digestConfig *safety.Var[DigestConfig]
func InitDigestCache() {
cachemanager.NewMemoryMapCache(nil, digestRaw, config.GetConfig().CacheTime.DigestCacheTime, "digestPlugin", func() time.Duration {
return config.GetConfig().CacheTime.DigestCacheTime
})
digestConfig = reload.VarsBy(func() DigestConfig {
c, err := config.GetCustomizedConfig[DigestConfig]()
if err != nil {
logs.Error(err, "get digest config fail")
c.DigestWordCount = config.GetConfig().DigestWordCount
c.DigestAllowTag = config.GetConfig().DigestAllowTag
return c
}
if c.DigestRegex != "" {
digest.SetQutos(c.DigestRegex)
}
if len(c.DigestTagOccupyNum) <= 1 {
return c
}
c.specialSolve = ParseDigestConf(c)
return c
}, "digestConfig")
}
func ParseDigestConf(c DigestConfig) map[string]digest.SpecialSolveConf {
specialSolve := map[string]digest.SpecialSolveConf{}
for _, item := range c.DigestTagOccupyNum {
tags := strings.Split(strings.ReplaceAll(item.Tag, " ", ""), "<")
for _, tag := range tags {
if tag == "" {
continue
}
ec := make(map[rune]digest.SpecialSolve)
specialTags := make(map[string]digest.SpecialSolve)
tag = str.Join("<", tag)
if len(item.EscapeCharacter) > 0 {
for _, esc := range item.EscapeCharacter {
for _, i := range esc.Character {
s := []rune(i)
if len(s) == 1 {
ec[s[0]] = digest.SpecialSolve{
Num: esc.Num,
ChuckOvered: esc.ChuckOvered,
}
}
}
if esc.Tags == "" {
continue
}
tagss := strings.Split(strings.ReplaceAll(esc.Tags, " ", ""), "<")
for _, t := range tagss {
if t == "" {
continue
}
t = str.Join("<", t)
specialTags[t] = digest.SpecialSolve{
Num: esc.Num,
ChuckOvered: esc.ChuckOvered,
}
}
}
}
v, ok := specialSolve[tag]
if !ok {
specialSolve[tag] = digest.SpecialSolveConf{
Num: item.Num,
ChuckOvered: item.ChuckOvered,
EscapeCharacter: ec,
Tags: specialTags,
}
continue
}
v.Num = item.Num
v.ChuckOvered = item.ChuckOvered
v.EscapeCharacter = maps.Merge(v.EscapeCharacter, ec)
v.Tags = maps.Merge(v.Tags, specialTags)
specialSolve[tag] = v
}
}
return specialSolve
}
func RemoveWpBlock(s string) string {
return removeWpBlock.ReplaceAllString(s, "")
}
func digestRaw(ctx context.Context, id uint64, arg ...any) (string, error) {
s := arg[1].(string)
limit := arg[3].(int)
if limit < 0 {
return s, nil
} else if limit == 0 {
return "", nil
}
s = more.ReplaceAllString(s, "")
fn := helper.GetContextVal(ctx, "postMoreFn", PostsMore)
return Digests(s, id, limit, fn), nil
}
func Digests(content string, id uint64, limit int, fn func(id uint64, content, closeTag string) string) string {
closeTag := ""
content = RemoveWpBlock(content)
c := digestConfig.Load()
tag := c.DigestAllowTag
if tag == "" {
tag = "<a><b><blockquote><br><cite><code><dd><del><div><dl><dt><em><h1><h2><h3><h4><h5><h6><i><img><li><ol><p><pre><span><strong><ul>"
}
content = digest.StripTags(content, tag)
length := utf8.RuneCountInString(content) + 1
if length <= limit {
return content
}
if len(c.specialSolve) > 0 {
content, closeTag = digest.CustomizeHtml(content, limit, c.specialSolve)
} else {
content, closeTag = digest.Html(content, limit)
}
if fn == nil {
return PostsMore(id, content, closeTag)
}
return fn(id, content, closeTag)
}
func PostsMore(id uint64, content, closeTag string) string {
tmp := `%s......%s<p class="read-more"><a href="/p/%d">继续阅读</a></p>`
if strings.Contains(closeTag, "pre") || strings.Contains(closeTag, "code") {
tmp = `%s%s......<p class="read-more"><a href="/p/%d">继续阅读</a></p>`
}
content = fmt.Sprintf(tmp, content, closeTag, id)
return content
}
func Digest(ctx context.Context, post *models.Posts, limit int) {
content, _ := cachemanager.GetBy[string]("digestPlugin", ctx, post.Id, time.Second, ctx, post.PostContent, post.Id, limit)
post.PostContent = content
}
func PostExcerpt(post *models.Posts) {
post.PostContent = strings.Replace(post.PostExcerpt, "\n", "<br/>", -1)
}

View File

@ -1,176 +0,0 @@
package plugins
import (
"fmt"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper"
str "github.com/fthvgb1/wp-go/helper/strings"
"net/url"
"regexp"
"strconv"
"strings"
)
type PageEle struct {
PrevEle string
NextEle string
DotsEle string
MiddleEle string
CurrentEle string
}
func TwentyFifteenPagination() PageEle {
return twentyFifteen
}
func TwentyFifteenCommentPagination() CommentPageEle {
return twentyFifteenComment
}
type CommentPageEle struct {
PageEle
}
var twentyFifteen = PageEle{
PrevEle: `<a class="prev page-numbers" href="%s">上一页</a>`,
NextEle: `<a class="next page-numbers" href="%s">下一页</a>`,
DotsEle: `<span class="page-numbers dots">…</span>`,
MiddleEle: `<a class="page-numbers" href="%s"><span class="meta-nav screen-reader-text"> </span>%d</a>
`,
CurrentEle: `<span aria-current="page" class="page-numbers current">
<span class="meta-nav screen-reader-text"> </span>%d</span>`,
}
var twentyFifteenComment = CommentPageEle{
PageEle{
PrevEle: `<div class="nav-previous"><a href="%s">%s</a></div>`,
NextEle: `<div class="nav-next"><a href="%s">%s</a></div>`,
},
}
func (p PageEle) Current(page, totalPage, totalRow int) string {
return fmt.Sprintf(p.CurrentEle, page)
}
func (p PageEle) Prev(url string) string {
return fmt.Sprintf(p.PrevEle, url)
}
func (p PageEle) Next(url string) string {
return fmt.Sprintf(p.NextEle, url)
}
func (p PageEle) Dots() string {
return p.DotsEle
}
func (p PageEle) Middle(page int, url string) string {
return fmt.Sprintf(p.MiddleEle, url, page)
}
var reg = regexp.MustCompile(`(/page)/(\d+)`)
var commentReg = regexp.MustCompile(`/comment-page-(\d+)`)
var queryParam = []string{"paged", "cat", "m", "author", "tag"}
func (p PageEle) Urls(u url.URL, page int, isTLS bool) string {
var path, query = u.Path, u.RawQuery
if path == "/" {
v := u.Query()
for _, q := range queryParam {
if v.Get(q) != "" {
v.Set("paged", strconv.Itoa(page))
return str.Join(path, "?", v.Encode())
}
}
}
if !strings.Contains(path, "/page/") {
path = fmt.Sprintf("%s%s", path, "/page/1")
}
if page == 1 {
path = reg.ReplaceAllString(path, "")
} else {
s := fmt.Sprintf("$1/%d", page)
path = reg.ReplaceAllString(path, s)
}
path = strings.Replace(path, "//", "/", -1)
if path == "" {
path = "/"
}
if query != "" {
return str.Join(path, "?", query)
}
return path
}
func (p CommentPageEle) Urls(u url.URL, page int, isTLS bool) string {
var path, query = u.Path, u.RawQuery
if path == "/" {
v := u.Query()
if v.Get("p") != "" {
v.Set("cpage", strconv.Itoa(page))
return str.Join(path, "?", v.Encode(), "#comments")
}
}
if !strings.Contains(path, "/comment-page-") {
path = fmt.Sprintf("%s%s", path, "/comment-page-1")
}
path = commentReg.ReplaceAllString(path, fmt.Sprintf("/comment-page-%d", page))
path = strings.Replace(path, "//", "/", -1)
ur := path
if query != "" {
ur = str.Join(path, "?", query)
}
ur = str.Join(ur, "#comments")
return ur
}
func (p CommentPageEle) Middle(page int, url string) string {
return ""
}
func (p CommentPageEle) Dots() string {
return ""
}
func (p CommentPageEle) Current(page, totalPage, totalRow int) string {
return ""
}
func (p CommentPageEle) Prev(url string) string {
return fmt.Sprintf(p.PrevEle, url, helper.Or(wpconfig.GetOption("comment_order") == "asc", "较早评论", "较新评论"))
}
func (p CommentPageEle) Next(url string) string {
return fmt.Sprintf(p.NextEle, url, helper.Or(wpconfig.GetOption("comment_order") == "asc", "较新评论", "较早评论"))
}
type PaginationNav struct {
Currents func(page, totalPage, totalRows int) string
Prevs func(url string) string
Nexts func(url string) string
Dotss func() string
Middles func(page int, url string) string
Urlss func(u url.URL, page int, isTLS bool) string
}
func (p PaginationNav) Current(page, totalPage, totalRows int) string {
return p.Currents(page, totalPage, totalRows)
}
func (p PaginationNav) Prev(url string) string {
return p.Prevs(url)
}
func (p PaginationNav) Next(url string) string {
return p.Nexts(url)
}
func (p PaginationNav) Dots() string {
return p.Dotss()
}
func (p PaginationNav) Middle(page int, url string) string {
return p.Middles(page, url)
}
func (p PaginationNav) Urls(u url.URL, page int, isTLS bool) string {
return p.Urlss(u, page, isTLS)
}

View File

@ -1,30 +0,0 @@
package apply
import "github.com/fthvgb1/wp-go/safety"
var contains = safety.NewMap[string, any]()
func SetVal(key string, val any) {
contains.Store(key, val)
}
func DelVal(key string) {
contains.Delete(key)
}
func GetVal[V any](key string) (V, bool) {
v, ok := contains.Load(key)
if !ok {
var vv V
return vv, ok
}
return v.(V), ok
}
func GetRawVal(key string) (any, bool) {
return contains.Load(key)
}
func GetPlugins() any {
v, _ := contains.Load("wp-plugins")
return v
}

View File

@ -1,83 +0,0 @@
package enlightjs
import (
"fmt"
"github.com/fthvgb1/wp-go/app/phphelper"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/goccy/go-json"
)
type Config struct {
Selectors Selectors `json:"selectors"`
Options Options `json:"options"`
}
type Options struct {
Indent int64 `json:"indent,omitempty"`
AmpersandCleanup bool `json:"ampersandCleanup,omitempty"`
Linehover bool `json:"linehover,omitempty"`
RawcodeDbclick bool `json:"rawcodeDbclick,omitempty"`
TextOverflow string `json:"textOverflow,omitempty"`
Linenumbers bool `json:"linenumbers,omitempty"`
Theme string `json:"theme,omitempty"`
Language string `json:"language,omitempty"`
RetainCssClasses bool `json:"retainCssClasses,omitempty"`
Collapse bool `json:"collapse,omitempty"`
ToolbarOuter string `json:"toolbarOuter,omitempty"`
ToolbarTop string `json:"toolbarTop,omitempty"`
ToolbarBottom string `json:"toolbarBottom,omitempty"`
}
type Selectors struct {
Block string `json:"block,omitempty"`
Inline string `json:"inline,omitempty"`
}
func EnlighterJS(h *wp.Handle) {
h.PushGroupHeadScript(constraints.AllScene, "enlighterjs-css", 20, `<link rel='stylesheet' id='enlighterjs-css' href='/wp-content/plugins/enlighter/cache/enlighterjs.min.css' media='all' />`)
h.PushCacheGroupFooterScript(constraints.AllScene, "enlighterJs", 10, func(h *wp.Handle) string {
op := wpconfig.GetOption("enlighter-options")
opp, err := phphelper.UnPHPSerializeToStrAnyMap(op)
if err != nil {
logs.Error(err, "获取enlighter-option失败", op)
return ""
}
v := Config{
Selectors: Selectors{
Block: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-selector-block", "pre.EnlighterJSRAW"),
Inline: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-selector-inline", "code.EnlighterJSRAW"),
},
Options: Options{
Indent: maps.GetStrAnyValWithDefaults[int64](opp, "enlighterjs-indent", 4),
AmpersandCleanup: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-ampersandcleanup", true),
Linehover: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-linehover", true),
RawcodeDbclick: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-rawcodedbclick", true),
TextOverflow: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-textoverflow", "break"),
Linenumbers: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-linenumbers", true),
Theme: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-theme", "enlighter"),
Language: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-language", "generic"),
RetainCssClasses: maps.GetStrAnyValWithDefaults(opp, "enlighterjs-retaincss", false),
Collapse: false,
ToolbarOuter: "",
ToolbarTop: "{BTN_RAW}{BTN_COPY}{BTN_WINDOW}{BTN_WEBSITE}",
ToolbarBottom: "",
},
}
conf, err := json.Marshal(v)
if err != nil {
logs.Error(err, "json化enlighterjs配置失败")
return ""
}
return fmt.Sprintf(enlighterjs, conf)
})
}
var enlighterjs = `<script src='/wp-content/plugins/enlighter/cache/enlighterjs.min.js?ver=0A0B0C' id='enlighterjs-js'></script>
<script id='enlighterjs-js-after'>
!function(e,n){if("undefined"!=typeof EnlighterJS){var o=%s;(e.EnlighterJSINIT=function(){EnlighterJS.init(o.selectors.block,o.selectors.inline,o.options)})()}else{(n&&(n.error||n.log)||function(){})("Error: EnlighterJS resources not loaded yet!")}}(window,console);
</script>`

View File

@ -1,72 +0,0 @@
package wphandle
import (
"errors"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/plugins/wphandle/apply"
"github.com/fthvgb1/wp-go/app/plugins/wphandle/enlightjs"
"github.com/fthvgb1/wp-go/app/plugins/wphandle/hiddenlogin"
"github.com/fthvgb1/wp-go/app/theme/wp"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"path/filepath"
"plugin"
)
var plugins = func() *safety.Map[string, func(*wp.Handle)] {
m := safety.NewMap[string, func(*wp.Handle)]()
m.Store("Enlightjs", enlightjs.EnlighterJS)
m.Store("HiddenLogin", hiddenlogin.HiddenLogin)
return m
}()
func RegisterPlugin(name string, fn func(*wp.Handle)) {
plugins.Store(name, fn)
}
func UsePlugins(h *wp.Handle, calls ...string) {
calls = append(calls, config.GetConfig().Plugins...)
for _, call := range calls {
call = str.FirstUpper(call)
if fn, ok := plugins.Load(call); ok {
fn(h)
}
}
}
func LoadPlugins() {
dirPath := config.GetConfig().PluginPath
if dirPath == "" {
return
}
glob, err := filepath.Glob(filepath.Join(dirPath, "*.so"))
if err != nil {
logs.Error(err, "读取插件目录错误", dirPath)
return
}
for _, entry := range glob {
p, err := plugin.Open(entry)
if err != nil {
logs.Error(err, "读取插件错误", entry)
continue
}
name := filepath.Ext(entry)
name = filepath.Base(entry[0 : len(entry)-len(name)])
name = str.FirstUpper(name)
pl, err := p.Lookup(name)
if err != nil {
logs.Error(err, "插件lookup错误", entry)
continue
}
plu, ok := pl.(func(*wp.Handle))
if !ok {
logs.Error(errors.New("switch func(*wp.Handle) fail"), "插件转换错误", entry)
continue
}
RegisterPlugin(name, plu)
}
apply.SetVal("wp-plugins", func(h *wp.Handle) {
UsePlugins(h)
})
}

View File

@ -1,17 +0,0 @@
package hiddenlogin
import (
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/theme/wp"
str "github.com/fthvgb1/wp-go/helper/strings"
)
func HiddenLogin(h *wp.Handle) {
h.AddActionFilter(widgets.Meta, func(h *wp.Handle, s string, args ...any) string {
return str.Replace(s, map[string]string{
`<li><a href="/wp-login.php">登录</a></li>`: "",
`<li><a href="/feed">登录</a></li>`: "",
`<li><a href="/comments/feed">登录</a></li>`: "",
})
})
}

View File

@ -1,22 +0,0 @@
package tests
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/theme/wp"
)
func Tt(h *wp.Handle) {
h.HookHandle(constraints.PipeMiddleware, constraints.AllScene, func(call wp.HandleCall) (wp.HandleCall, bool) {
return call, false
})
/*h.PushPipeHook(constraints.Home, func(pipe wp.Pipe) (wp.Pipe, bool) {
return wp.Pipe{}, false
})*/
//h.DeletePipe(constraints.Home, constraints.PipeMiddleware)
h.ReplacePipe(constraints.Home, constraints.PipeMiddleware, wp.NewPipe("log", 500, func(next wp.HandleFn[*wp.Handle], h *wp.Handle) {
fmt.Println("ffff")
next(h)
fmt.Println("iiiii")
}))
}

View File

@ -1,147 +0,0 @@
package route
import (
"github.com/fthvgb1/wp-go/app/actions"
"github.com/fthvgb1/wp-go/app/middleware"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/theme"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/slice"
"github.com/fthvgb1/wp-go/helper/slice/mockmap"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/gin-contrib/gzip"
"github.com/gin-contrib/pprof"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
)
type GinSetter func(*gin.Engine)
var setters mockmap.Map[string, GinSetter]
var setterHooks []func(item mockmap.Item[string, GinSetter]) (mockmap.Item[string, GinSetter], bool)
// SetGinAction 方便插件在init时使用
func SetGinAction(name string, hook GinSetter, orders ...float64) {
setters.Set(name, hook, orders...)
}
func HookGinSetter(fn func(item mockmap.Item[string, GinSetter]) (mockmap.Item[string, GinSetter], bool)) {
setterHooks = append(setterHooks, fn)
}
// DelGinSetter 方便插件在init时使用
func DelGinSetter(name string) {
setterHooks = append(setterHooks, func(item mockmap.Item[string, GinSetter]) (mockmap.Item[string, GinSetter], bool) {
return item, item.Name != name
})
}
func SetupRouter() *gin.Engine {
// Disable Console Color
// gin.DisableConsoleColor()
r := gin.New()
c := config.GetConfig()
SetGinAction("initTrustIp", func(r *gin.Engine) {
if len(c.TrustIps) > 0 {
err := r.SetTrustedProxies(c.TrustIps)
if err != nil {
panic(err)
}
}
}, 99.5)
SetGinAction("setTemplate", func(r *gin.Engine) {
r.HTMLRender = theme.BuildTemplate()
wpconfig.SetTemplateFs(theme.TemplateFs)
}, 90.5)
siteFlowLimitMiddleware, siteFlow := middleware.FlowLimit(c.MaxRequestSleepNum, c.MaxRequestNum, c.CacheTime.SleepTime)
reload.Append(func() {
c = config.GetConfig()
siteFlow(c.MaxRequestSleepNum, c.MaxRequestNum, c.CacheTime.SleepTime)
}, "site-flowLimit-config")
SetGinAction("setGlobalMiddleware", func(r *gin.Engine) {
r.Use(
gin.Logger(),
middleware.ValidateServerNames(),
middleware.RecoverAndSendMail(gin.DefaultErrorWriter),
siteFlowLimitMiddleware,
middleware.SetStaticFileCache,
)
}, 88.5)
SetGinAction("setGzip", func(r *gin.Engine) {
//gzip 因为一般会用nginx做反代时自动使用gzip,所以go这边本身可以不用
if c.Gzip {
r.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{
"/wp-includes/", "/wp-content/",
})))
}
}, 87.6)
SetGinAction("setWpDir", func(r *gin.Engine) {
if c.WpDir == "" {
panic("wordpress path can't be empty")
}
r.Static("/wp-content/uploads", str.Join(c.WpDir, "/wp-content/uploads"))
r.Static("/wp-content/themes", str.Join(c.WpDir, "/wp-content/themes"))
r.Static("/wp-content/plugins", str.Join(c.WpDir, "/wp-content/plugins"))
r.Static("/wp-includes/css", str.Join(c.WpDir, "/wp-includes/css"))
r.Static("/wp-includes/fonts", str.Join(c.WpDir, "/wp-includes/fonts"))
r.Static("/wp-includes/js", str.Join(c.WpDir, "/wp-includes/js"))
}, 86.1)
SetGinAction("setSession", func(r *gin.Engine) {
store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("go-wp", store))
}, 85.1)
SetGinAction("setRoute", func(r *gin.Engine) {
r.GET("/", actions.Feed, middleware.SearchLimit(c.SingleIpSearchNum),
actions.ThemeHook(constraints.Home))
r.GET("/page/:page", actions.ThemeHook(constraints.Home))
r.GET("/p/category/:category", actions.ThemeHook(constraints.Category))
r.GET("/p/category/:category/page/:page", actions.ThemeHook(constraints.Category))
r.GET("/p/tag/:tag", actions.ThemeHook(constraints.Tag))
r.GET("/p/tag/:tag/page/:page", actions.ThemeHook(constraints.Tag))
r.GET("/p/date/:year/:month", actions.ThemeHook(constraints.Archive))
r.GET("/p/date/:year/:month/page/:page", actions.ThemeHook(constraints.Archive))
r.GET("/p/author/:author", actions.ThemeHook(constraints.Author))
r.GET("/p/author/:author/page/:page", actions.ThemeHook(constraints.Author))
r.POST("/login", actions.Login)
r.GET("/p/:id", actions.ThemeHook(constraints.Detail))
r.GET("/p/:id/comment-page-:page", actions.ThemeHook(constraints.Detail))
r.GET("/p/:id/feed", actions.PostFeed)
r.GET("/feed", actions.SiteFeed)
r.GET("/comments/feed", actions.CommentsFeed)
commentMiddleWare, _ := middleware.FlowLimit(c.MaxRequestSleepNum, 5, c.CacheTime.SleepTime)
r.POST("/comment", commentMiddleWare, actions.PostComment)
r.NoRoute(actions.ThemeHook(constraints.NoRoute))
}, 84.6)
SetGinAction("setpprof", func(r *gin.Engine) {
if c.Pprof != "" {
pprof.Register(r, c.Pprof)
}
}, 80.8)
for _, hook := range setterHooks {
setters = slice.FilterAndMap(setters, hook)
}
slice.SimpleSort(setters, slice.DESC, func(t mockmap.Item[string, GinSetter]) float64 {
return t.Order
})
for _, fn := range setters {
fn.Value(r)
}
return r
}

View File

@ -1,82 +0,0 @@
package theme
import (
"embed"
"github.com/fthvgb1/wp-go/multipTemplate"
"github.com/fthvgb1/wp-go/safety"
"html/template"
"io/fs"
"path/filepath"
"strings"
)
//go:embed *[^.go]
var TemplateFs embed.FS
var templates = safety.NewMap[string, *template.Template]() //方便外部获取模板render后的字符串不然在gin中获取不了
var multiple *multipTemplate.MultipleFsTemplate
func BuildTemplate() *multipTemplate.MultipleFsTemplate {
if multiple != nil {
tt := multipTemplate.NewFsTemplate(TemplateFs)
commonTemplate(tt)
for k, v := range map[string]*template.Template(any(tt.Template).(multipTemplate.TemplateMaps)) {
multiple.Template.Store(k, v)
}
} else {
multiple = multipTemplate.NewFsTemplates(TemplateFs, templates)
commonTemplate(multiple)
}
/*t.AddTemplate("twentyfifteen/*[^layout]/*.gohtml", FuncMap(), "twentyfifteen/layout/*.gohtml","wp/template.gohtml"). //单个主题设置
AddTemplate("twentyseventeen/*[^layout]/*.gohtml", FuncMap(), "twentyseventeen/layout/*.gohtml","wp/template.gohtml")*/
return multiple
}
func GetMultipleTemplate() *multipTemplate.MultipleFsTemplate {
if multiple == nil {
BuildTemplate()
}
return multiple
}
func GetTemplate(name string) (*template.Template, bool) {
t, ok := templates.Load(name)
return t, ok
}
// 所有主题模板通用设置
func commonTemplate(t *multipTemplate.MultipleFsTemplate) {
m, err := fs.Glob(t.Fs, "*/posts/*.gohtml")
if err != nil {
panic(err)
}
funMap := FuncMap()
for _, main := range m {
file := filepath.Base(main)
dir := strings.Split(main, "/")[0]
templ := template.Must(template.New(file).Funcs(funMap).ParseFS(t.Fs, main, filepath.Join(dir, "layout/*.gohtml"), "wp/template.gohtml"))
t.SetTemplate(main, templ)
}
}
func IsTemplateDirExists(tml string) bool {
arr, err := TemplateFs.ReadDir(tml)
if err != nil {
return false
}
if len(arr) > 0 {
return true
}
return false
}
func IsTemplateExists(tml string) bool {
t, ok := templates.Load(tml)
return ok && t != nil
}
func SetTemplate(name string, val *template.Template) {
templates.Store(name, val)
}

View File

@ -1,35 +0,0 @@
package theme
import (
"github.com/fthvgb1/wp-go/app/theme/wp"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
)
var themeMap = safety.NewMap[string, func(*wp.Handle)]()
func AddTheme(name string, fn func(handle *wp.Handle)) {
themeMap.Store(name, fn)
}
func DelTheme(name string) {
themeMap.Delete(name)
}
func GetTheme(name string) (func(*wp.Handle), bool) {
return themeMap.Load(name)
}
func IsThemeHookFuncExist(name string) bool {
_, ok := themeMap.Load(name)
return ok
}
func Hook(themeName string, h *wp.Handle) {
fn, ok := themeMap.Load(themeName)
if ok && fn != nil {
fn(h)
return
}
panic(str.Join("theme ", themeName, " don't exist"))
}

View File

@ -1,37 +0,0 @@
package theme
import (
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"html/template"
"time"
)
func postsFn(fn func(models.Posts) string, a models.Posts) string {
return fn(a)
}
func FuncMap() template.FuncMap {
return template.FuncMap{
"unescaped": func(s string) any {
return template.HTML(s)
},
"dateCh": func(t time.Time) any {
return t.Format("2006年 01月 02日")
},
"timeFormat": func(t time.Time, format string) any {
return t.Format(format)
},
"getOption": func(k string) string {
return wpconfig.GetOption(k)
},
"getLang": wpconfig.GetLang,
"postsFn": postsFn,
"exec": func(fn func() string) template.HTML {
return template.HTML(fn())
},
"callFuncString": func(fn func(string) string, s string) template.HTML {
return template.HTML(fn(s))
},
}
}

View File

@ -1,24 +0,0 @@
package theme
import (
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/theme/twentyfifteen"
"github.com/fthvgb1/wp-go/app/theme/twentyseventeen"
"github.com/fthvgb1/wp-go/app/wpconfig"
)
func InitTheme() {
AddTheme(twentyfifteen.ThemeName, twentyfifteen.Hook)
AddTheme(twentyseventeen.ThemeName, twentyseventeen.Hook)
}
func GetCurrentTheme() string {
themeName := config.GetConfig().Theme
if themeName == "" {
themeName = wpconfig.GetOption("template")
}
if !IsTemplateDirExists(themeName) {
themeName = "twentyfifteen"
}
return themeName
}

View File

@ -1,5 +0,0 @@
{{define "layout/sidebar" }}
<div id="widget-area" class="widget-area" role="complementary">
{{template "common/sidebarWidget" .}}
</div>
{{end}}

View File

@ -1,295 +0,0 @@
package twentyfifteen
import "github.com/fthvgb1/wp-go/app/wpconfig"
type themeSupport struct {
CustomBackground customBackground `json:"custom-background"`
EditorColorPalette []EditorColorPalette `json:"editor-color-palette"`
EditorGradientPresets []EditorGradientPresets `json:"editor-gradient-presets"`
}
type customBackground struct {
DefaultImage string `json:"default-image"`
DefaultPreset string `json:"default-preset"`
DefaultPositionX string `json:"default-position-x"`
DefaultPositionY string `json:"default-position-y"`
DefaultSize string `json:"default-size"`
DefaultRepeat string `json:"default-repeat"`
DefaultAttachment string `json:"default-attachment"`
DefaultColor string `json:"default-color"`
WpHeadCallback string `json:"wp-head-callback"`
AdminHeadCallback string `json:"admin-head-callback"`
AdminPreviewCallback string `json:"admin-preview-callback"`
}
type EditorColorPalette struct {
Name string `json:"name"`
Slug string `json:"slug"`
Color string `json:"color"`
}
type EditorGradientPresets struct {
Name string `json:"name"`
Slug string `json:"slug"`
Gradient string `json:"gradient"`
}
var themesupport = themeSupport{
CustomBackground: customBackground{
DefaultImage: "",
DefaultPreset: "default",
DefaultPositionX: "left",
DefaultPositionY: "top",
DefaultSize: "auto",
DefaultRepeat: "repeat",
DefaultAttachment: "fixed",
DefaultColor: "f1f1f1",
WpHeadCallback: "_custom_background_cb",
AdminHeadCallback: "",
AdminPreviewCallback: "",
},
EditorColorPalette: []EditorColorPalette{
{
Name: "暗灰色",
Slug: "dark-gray",
Color: "#111",
},
{
Name: "亮灰色",
Slug: "light-gray",
Color: "#f1f1f1",
},
{
Name: "白色",
Slug: "white",
Color: "#fff",
},
{
Name: "黄色",
Slug: "yellow",
Color: "#f4ca16",
},
{
Name: "暗棕色",
Slug: "dark-brown",
Color: "#352712",
},
{
Name: "粉色",
Slug: "medium-pink",
Color: "#e53b51",
},
{
Name: "浅粉色",
Slug: "light-pink",
Color: "#ffe5d1",
},
{
Name: "暗紫色",
Slug: "dark-purple",
Color: "#2e2256",
},
{
Name: "紫色",
Slug: "purple",
Color: "#674970",
},
{
Name: "蓝灰色",
Slug: "blue-gray",
Color: "#22313f",
},
{
Name: "亮蓝色",
Slug: "bright-blue",
Color: "#55c3dc",
},
{
Name: "浅蓝色",
Slug: "light-blue",
Color: "#e9f2f9",
},
},
EditorGradientPresets: []EditorGradientPresets{
{
Name: "Dark Gray Gradient",
Slug: "dark-gray-gradient-gradient",
Gradient: "linear-gradient(90deg, rgba(17,17,17,1) 0%, rgba(42,42,42,1) 100%)",
},
{
Name: "Light Gray Gradient",
Slug: "light-gray-gradient",
Gradient: "linear-gradient(90deg, rgba(241,241,241,1) 0%, rgba(215,215,215,1) 100%)",
},
{
Name: "White Gradient",
Slug: "white-gradient",
Gradient: "linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(230,230,230,1) 100%)",
},
{
Name: "Yellow Gradient",
Slug: "yellow-gradient",
Gradient: "linear-gradient(90deg, rgba(244,202,22,1) 0%, rgba(205,168,10,1) 100%)",
},
{
Name: "Dark Brown Gradient",
Slug: "dark-brown-gradient",
Gradient: "linear-gradient(90deg, rgba(53,39,18,1) 0%, rgba(91,67,31,1) 100%)",
},
{
Name: "Medium Pink Gradient",
Slug: "medium-pink-gradient",
Gradient: "linear-gradient(90deg, rgba(229,59,81,1) 0%, rgba(209,28,51,1) 100%)",
},
{
Name: "Light Pink Gradient",
Slug: "light-pink-gradient",
Gradient: "linear-gradient(90deg, rgba(255,229,209,1) 0%, rgba(255,200,158,1) 100%)",
},
{
Name: "Dark Purple Gradient",
Slug: "dark-purple-gradient",
Gradient: "linear-gradient(90deg, rgba(46,34,86,1) 0%, rgba(66,48,123,1) 100%)",
},
{
Name: "Purple Gradient",
Slug: "purple-gradient",
Gradient: "linear-gradient(90deg, rgba(103,73,112,1) 0%, rgba(131,93,143,1) 100%)",
},
{
Name: "Blue Gray Gradient",
Slug: "blue-gray-gradient",
Gradient: "linear-gradient(90deg, rgba(34,49,63,1) 0%, rgba(52,75,96,1) 100%)",
},
{
Name: "Bright Blue Gradient",
Slug: "bright-blue-gradient",
Gradient: "linear-gradient(90deg, rgba(85,195,220,1) 0%, rgba(43,180,211,1) 100%)",
},
{
Name: "Light Blue Gradient",
Slug: "light-blue-gradient",
Gradient: "linear-gradient(90deg, rgba(233,242,249,1) 0%, rgba(193,218,238,1) 100%)",
},
},
}
var colorscheme = map[string]ColorScheme{
"default": {
Label: "Default",
Colors: []string{
"#f1f1f1",
"#ffffff",
"#ffffff",
"#333333",
"#333333",
"#f7f7f7",
},
},
"dark": {
Label: "Dark",
Colors: []string{
"#111111",
"#202020",
"#202020",
"#bebebe",
"#bebebe",
"#1b1b1b",
},
},
"pink": {
Label: "Pink",
Colors: []string{
"#ffe5d1",
"#e53b51",
"#ffffff",
"#352712",
"#ffffff",
"#f1f1f1",
},
},
"purple": {
Label: "Purple",
Colors: []string{
"#674970",
"#2e2256",
"#ffffff",
"#2e2256",
"#ffffff",
"#f1f1f1",
},
},
"blue": {
Label: "Blue",
Colors: []string{
"#e9f2f9",
"#55c3dc",
"#ffffff",
"#22313f",
"#ffffff",
"#f1f1f1",
},
},
}
var _ = func() struct{} {
v := wpconfig.ThemeSupport{
CoreBlockPatterns: true,
WidgetsBlockEditor: true,
AutomaticFeedLinks: true,
TitleTag: true,
PostThumbnails: true,
Menus: true,
HTML5: []string{
"search-form",
"comment-form",
"comment-list",
"gallery",
"caption",
"script",
"style",
"navigation-widgets",
},
PostFormats: []string{
"aside",
"image",
"video",
"quote",
"link",
"gallery",
"status",
"audio",
"chat",
},
CustomLogo: wpconfig.CustomLogo{
Width: 248,
Height: 248,
FlexWidth: false,
FlexHeight: true,
HeaderText: "",
UnlinkHomepageLogo: false,
},
CustomizeSelectiveRefreshWidgets: true,
EditorStyle: true,
EditorStyles: true,
WpBlockStyles: true,
ResponsiveEmbeds: true,
CustomHeader: wpconfig.CustomHeader{
DefaultImage: "",
RandomDefault: false,
Width: 954,
Height: 1300,
FlexHeight: false,
FlexWidth: false,
DefaultTextColor: "333333",
HeaderText: true,
Uploads: true,
WpHeadCallback: "twentyfifteen_header_style",
AdminHeadCallback: "",
AdminPreviewCallback: "",
Video: false,
VideoActiveCallback: "is_front_page",
},
Widgets: true,
}
wpconfig.SetThemeSupport(ThemeName, v)
return struct{}{}
}()

View File

@ -1,63 +0,0 @@
package twentyfifteen
import (
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components"
"github.com/fthvgb1/wp-go/app/theme/wp/middleware"
"github.com/fthvgb1/wp-go/app/wpconfig"
"strings"
)
const ThemeName = "twentyfifteen"
func Hook(h *wp.Handle) {
wp.Run(h, configs)
}
func configs(h *wp.Handle) {
h.AddActionFilter(widgets.Search, func(h *wp.Handle, s string, args ...any) string {
return strings.ReplaceAll(s, `class="search-submit"`, `class="search-submit screen-reader-text"`)
})
wp.InitPipe(h)
middleware.CommonMiddleware(h)
setPaginationAndRender(h)
h.PushCacheGroupHeadScript(constraints.AllScene, "CalCustomBackGround", 10.005, CalCustomBackGround)
h.PushCacheGroupHeadScript(constraints.AllScene, "colorSchemeCss", 10.0056, colorSchemeCss)
h.CommonComponents()
components.WidgetArea(h)
h.PushRender(constraints.AllScene, wp.NewHandleFn(renderCustomHeader, 20.5, "renderCustomHeader"))
wp.PushIndexHandler(constraints.PipeRender, h, wp.NewHandleFn(wp.IndexRender, 50.005, "wp.IndexRender"))
h.PushRender(constraints.Detail, wp.NewHandleFn(wp.DetailRender, 50.005, "wp.DetailRender"))
h.PushRender(constraints.Detail, wp.NewHandleFn(postThumb, 60.005, "postThumb"))
h.PushDataHandler(constraints.Detail, wp.NewHandleFn(wp.Detail, 100.005, "wp.Detail"))
wp.PushIndexHandler(constraints.PipeData, h, wp.NewHandleFn(wp.Index, 100.005, "wp.Index"))
h.PushDataHandler(constraints.AllScene, wp.NewHandleFn(wp.PreCodeAndStats, 80.005, "wp.PreCodeAndStats"))
h.PushRender(constraints.AllScene, wp.NewHandleFn(wp.PreTemplate, 70.005, "wp.PreTemplate"))
}
func setPaginationAndRender(h *wp.Handle) {
h.PushHandler(constraints.PipeRender, constraints.Detail, wp.NewHandleFn(func(hh *wp.Handle) {
d := hh.GetDetailHandle()
d.CommentRender = plugins.CommentRender()
d.CommentPageEle = plugins.TwentyFifteenCommentPagination()
}, 150, "setPaginationAndRender"))
wp.PushIndexHandler(constraints.PipeRender, h, wp.NewHandleFn(func(hh *wp.Handle) {
i := hh.GetIndexHandle()
i.SetPageEle(plugins.TwentyFifteenPagination())
}, 150, "setPaginationAndRender"))
}
func postThumb(h *wp.Handle) {
d := h.GetDetailHandle()
if d.Post.Thumbnail.Path != "" {
d.Post.Thumbnail = wpconfig.Thumbnail(d.Post.Thumbnail.OriginAttachmentData, "post-thumbnail", "")
}
}
func renderCustomHeader(h *wp.Handle) {
h.SetData("customHeader", customHeader(h))
}

View File

@ -1,574 +0,0 @@
package twentyseventeen
import (
"fmt"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper/number"
str "github.com/fthvgb1/wp-go/helper/strings"
)
func colorScheme(h *wp.Handle) (r string) {
if "custom" != wpconfig.GetThemeModsVal(ThemeName, "colorscheme", "light") {
return
}
s := str.NewBuilder()
hue := number.ToString(wpconfig.GetThemeModsVal[int64](ThemeName, "colorscheme_hue", 250))
reducedSaturation := fmt.Sprintf("%d%%", int(.8*50))
saturation := fmt.Sprintf("%d%%", 50)
css := str.Replace(customCss, map[string]string{
"' . $hue . '": hue,
"' . esc_attr( $hue ) . '": hue,
"' . $saturation . '": saturation,
"' . esc_attr( $saturation ) .' ": saturation,
"' . $reduced_saturation . '": reducedSaturation,
})
s.Sprintf(`<style type="text/css" id="custom-theme-colors">%s</style>`, css)
r = s.String()
return
}
var customCss = `
/**
* Twenty Seventeen: Color Patterns
*
* Colors are ordered from dark to light.
*/
.colors-custom a:hover,
.colors-custom a:active,
.colors-custom .entry-content a:focus,
.colors-custom .entry-content a:hover,
.colors-custom .entry-summary a:focus,
.colors-custom .entry-summary a:hover,
.colors-custom .comment-content a:focus,
.colors-custom .comment-content a:hover,
.colors-custom .widget a:focus,
.colors-custom .widget a:hover,
.colors-custom .site-footer .widget-area a:focus,
.colors-custom .site-footer .widget-area a:hover,
.colors-custom .posts-navigation a:focus,
.colors-custom .posts-navigation a:hover,
.colors-custom .comment-metadata a:focus,
.colors-custom .comment-metadata a:hover,
.colors-custom .comment-metadata a.comment-edit-link:focus,
.colors-custom .comment-metadata a.comment-edit-link:hover,
.colors-custom .comment-reply-link:focus,
.colors-custom .comment-reply-link:hover,
.colors-custom .widget_authors a:focus strong,
.colors-custom .widget_authors a:hover strong,
.colors-custom .entry-title a:focus,
.colors-custom .entry-title a:hover,
.colors-custom .entry-meta a:focus,
.colors-custom .entry-meta a:hover,
.colors-custom.blog .entry-meta a.post-edit-link:focus,
.colors-custom.blog .entry-meta a.post-edit-link:hover,
.colors-custom.archive .entry-meta a.post-edit-link:focus,
.colors-custom.archive .entry-meta a.post-edit-link:hover,
.colors-custom.search .entry-meta a.post-edit-link:focus,
.colors-custom.search .entry-meta a.post-edit-link:hover,
.colors-custom .page-links a:focus .page-number,
.colors-custom .page-links a:hover .page-number,
.colors-custom .entry-footer a:focus,
.colors-custom .entry-footer a:hover,
.colors-custom .entry-footer .cat-links a:focus,
.colors-custom .entry-footer .cat-links a:hover,
.colors-custom .entry-footer .tags-links a:focus,
.colors-custom .entry-footer .tags-links a:hover,
.colors-custom .post-navigation a:focus,
.colors-custom .post-navigation a:hover,
.colors-custom .pagination a:not(.prev):not(.next):focus,
.colors-custom .pagination a:not(.prev):not(.next):hover,
.colors-custom .comments-pagination a:not(.prev):not(.next):focus,
.colors-custom .comments-pagination a:not(.prev):not(.next):hover,
.colors-custom .logged-in-as a:focus,
.colors-custom .logged-in-as a:hover,
.colors-custom a:focus .nav-title,
.colors-custom a:hover .nav-title,
.colors-custom .edit-link a:focus,
.colors-custom .edit-link a:hover,
.colors-custom .site-info a:focus,
.colors-custom .site-info a:hover,
.colors-custom .widget .widget-title a:focus,
.colors-custom .widget .widget-title a:hover,
.colors-custom .widget ul li a:focus,
.colors-custom .widget ul li a:hover {
color: hsl( ' . $hue . ', ' . $saturation . ', 0% ); /* base: #000; */
}
.colors-custom .entry-content a,
.colors-custom .entry-summary a,
.colors-custom .comment-content a,
.colors-custom .widget a,
.colors-custom .site-footer .widget-area a,
.colors-custom .posts-navigation a,
.colors-custom .widget_authors a strong {
-webkit-box-shadow: inset 0 -1px 0 hsl( ' . $hue . ', ' . $saturation . ', 6% ); /* base: rgba(15, 15, 15, 1); */
box-shadow: inset 0 -1px 0 hsl( ' . $hue . ', ' . $saturation . ', 6% ); /* base: rgba(15, 15, 15, 1); */
}
.colors-custom button,
.colors-custom input[type="button"],
.colors-custom input[type="submit"],
.colors-custom .entry-footer .edit-link a.post-edit-link {
background-color: hsl( ' . $hue . ', ' . $saturation . ', 13% ); /* base: #222; */
}
.colors-custom input[type="text"]:focus,
.colors-custom input[type="email"]:focus,
.colors-custom input[type="url"]:focus,
.colors-custom input[type="password"]:focus,
.colors-custom input[type="search"]:focus,
.colors-custom input[type="number"]:focus,
.colors-custom input[type="tel"]:focus,
.colors-custom input[type="range"]:focus,
.colors-custom input[type="date"]:focus,
.colors-custom input[type="month"]:focus,
.colors-custom input[type="week"]:focus,
.colors-custom input[type="time"]:focus,
.colors-custom input[type="datetime"]:focus,
.colors-custom .colors-custom input[type="datetime-local"]:focus,
.colors-custom input[type="color"]:focus,
.colors-custom textarea:focus,
.colors-custom button.secondary,
.colors-custom input[type="reset"],
.colors-custom input[type="button"].secondary,
.colors-custom input[type="reset"].secondary,
.colors-custom input[type="submit"].secondary,
.colors-custom a,
.colors-custom .site-title,
.colors-custom .site-title a,
.colors-custom .navigation-top a,
.colors-custom .dropdown-toggle,
.colors-custom .menu-toggle,
.colors-custom .page .panel-content .entry-title,
.colors-custom .page-title,
.colors-custom.page:not(.twentyseventeen-front-page) .entry-title,
.colors-custom .page-links a .page-number,
.colors-custom .comment-metadata a.comment-edit-link,
.colors-custom .comment-reply-link .icon,
.colors-custom h2.widget-title,
.colors-custom mark,
.colors-custom .post-navigation a:focus .icon,
.colors-custom .post-navigation a:hover .icon,
.colors-custom .site-content .site-content-light,
.colors-custom .twentyseventeen-panel .recent-posts .entry-header .edit-link {
color: hsl( ' . $hue . ', ' . $saturation . ', 13% ); /* base: #222; */
}
.colors-custom .entry-content a:focus,
.colors-custom .entry-content a:hover,
.colors-custom .entry-summary a:focus,
.colors-custom .entry-summary a:hover,
.colors-custom .comment-content a:focus,
.colors-custom .comment-content a:hover,
.colors-custom .widget a:focus,
.colors-custom .widget a:hover,
.colors-custom .site-footer .widget-area a:focus,
.colors-custom .site-footer .widget-area a:hover,
.colors-custom .posts-navigation a:focus,
.colors-custom .posts-navigation a:hover,
.colors-custom .comment-metadata a:focus,
.colors-custom .comment-metadata a:hover,
.colors-custom .comment-metadata a.comment-edit-link:focus,
.colors-custom .comment-metadata a.comment-edit-link:hover,
.colors-custom .comment-reply-link:focus,
.colors-custom .comment-reply-link:hover,
.colors-custom .widget_authors a:focus strong,
.colors-custom .widget_authors a:hover strong,
.colors-custom .entry-title a:focus,
.colors-custom .entry-title a:hover,
.colors-custom .entry-meta a:focus,
.colors-custom .entry-meta a:hover,
.colors-custom.blog .entry-meta a.post-edit-link:focus,
.colors-custom.blog .entry-meta a.post-edit-link:hover,
.colors-custom.archive .entry-meta a.post-edit-link:focus,
.colors-custom.archive .entry-meta a.post-edit-link:hover,
.colors-custom.search .entry-meta a.post-edit-link:focus,
.colors-custom.search .entry-meta a.post-edit-link:hover,
.colors-custom .page-links a:focus .page-number,
.colors-custom .page-links a:hover .page-number,
.colors-custom .entry-footer .cat-links a:focus,
.colors-custom .entry-footer .cat-links a:hover,
.colors-custom .entry-footer .tags-links a:focus,
.colors-custom .entry-footer .tags-links a:hover,
.colors-custom .post-navigation a:focus,
.colors-custom .post-navigation a:hover,
.colors-custom .pagination a:not(.prev):not(.next):focus,
.colors-custom .pagination a:not(.prev):not(.next):hover,
.colors-custom .comments-pagination a:not(.prev):not(.next):focus,
.colors-custom .comments-pagination a:not(.prev):not(.next):hover,
.colors-custom .logged-in-as a:focus,
.colors-custom .logged-in-as a:hover,
.colors-custom a:focus .nav-title,
.colors-custom a:hover .nav-title,
.colors-custom .edit-link a:focus,
.colors-custom .edit-link a:hover,
.colors-custom .site-info a:focus,
.colors-custom .site-info a:hover,
.colors-custom .widget .widget-title a:focus,
.colors-custom .widget .widget-title a:hover,
.colors-custom .widget ul li a:focus,
.colors-custom .widget ul li a:hover {
-webkit-box-shadow: inset 0 0 0 hsl( ' . $hue . ', ' . $saturation . ', 13% ), 0 3px 0 hsl( ' . $hue . ', ' . $saturation . ', 13% );
box-shadow: inset 0 0 0 hsl( ' . $hue . ', ' . $saturation . ' , 13% ), 0 3px 0 hsl( ' . $hue . ', ' . $saturation . ', 13% );
}
body.colors-custom,
.colors-custom button,
.colors-custom input,
.colors-custom select,
.colors-custom textarea,
.colors-custom h3,
.colors-custom h4,
.colors-custom h6,
.colors-custom label,
.colors-custom .entry-title a,
.colors-custom.twentyseventeen-front-page .panel-content .recent-posts article,
.colors-custom .entry-footer .cat-links a,
.colors-custom .entry-footer .tags-links a,
.colors-custom .format-quote blockquote,
.colors-custom .nav-title,
.colors-custom .comment-body,
.colors-custom .site-content .wp-playlist-light .wp-playlist-current-item .wp-playlist-item-album {
color: hsl( ' . $hue . ', ' . $reduced_saturation . ', 20% ); /* base: #333; */
}
.colors-custom .social-navigation a:hover,
.colors-custom .social-navigation a:focus {
background: hsl( ' . $hue . ', ' . $reduced_saturation . ', 20% ); /* base: #333; */
}
.colors-custom input[type="text"]:focus,
.colors-custom input[type="email"]:focus,
.colors-custom input[type="url"]:focus,
.colors-custom input[type="password"]:focus,
.colors-custom input[type="search"]:focus,
.colors-custom input[type="number"]:focus,
.colors-custom input[type="tel"]:focus,
.colors-custom input[type="range"]:focus,
.colors-custom input[type="date"]:focus,
.colors-custom input[type="month"]:focus,
.colors-custom input[type="week"]:focus,
.colors-custom input[type="time"]:focus,
.colors-custom input[type="datetime"]:focus,
.colors-custom input[type="datetime-local"]:focus,
.colors-custom input[type="color"]:focus,
.colors-custom textarea:focus,
.bypostauthor > .comment-body > .comment-meta > .comment-author .avatar {
border-color: hsl( ' . $hue . ', ' . $reduced_saturation . ', 20% ); /* base: #333; */
}
.colors-custom h2,
.colors-custom blockquote,
.colors-custom input[type="text"],
.colors-custom input[type="email"],
.colors-custom input[type="url"],
.colors-custom input[type="password"],
.colors-custom input[type="search"],
.colors-custom input[type="number"],
.colors-custom input[type="tel"],
.colors-custom input[type="range"],
.colors-custom input[type="date"],
.colors-custom input[type="month"],
.colors-custom input[type="week"],
.colors-custom input[type="time"],
.colors-custom input[type="datetime"],
.colors-custom input[type="datetime-local"],
.colors-custom input[type="color"],
.colors-custom textarea,
.colors-custom .site-description,
.colors-custom .entry-content blockquote.alignleft,
.colors-custom .entry-content blockquote.alignright,
.colors-custom .colors-custom .taxonomy-description,
.colors-custom .site-info a,
.colors-custom .wp-caption,
.colors-custom .gallery-caption {
color: hsl( ' . $hue . ', ' . $saturation . ', 40% ); /* base: #666; */
}
.colors-custom abbr,
.colors-custom acronym {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 40% ); /* base: #666; */
}
.colors-custom h5,
.colors-custom .entry-meta,
.colors-custom .entry-meta a,
.colors-custom.blog .entry-meta a.post-edit-link,
.colors-custom.archive .entry-meta a.post-edit-link,
.colors-custom.search .entry-meta a.post-edit-link,
.colors-custom .nav-subtitle,
.colors-custom .comment-metadata,
.colors-custom .comment-metadata a,
.colors-custom .no-comments,
.colors-custom .comment-awaiting-moderation,
.colors-custom .page-numbers.current,
.colors-custom .page-links .page-number,
.colors-custom .navigation-top .current-menu-item > a,
.colors-custom .navigation-top .current_page_item > a,
.colors-custom .main-navigation a:hover,
.colors-custom .site-content .wp-playlist-light .wp-playlist-current-item .wp-playlist-item-artist {
color: hsl( ' . $hue . ', ' . $saturation . ', 46% ); /* base: #767676; */
}
.colors-custom :not( .mejs-button ) > button:hover,
.colors-custom :not( .mejs-button ) > button:focus,
.colors-custom input[type="button"]:hover,
.colors-custom input[type="button"]:focus,
.colors-custom input[type="submit"]:hover,
.colors-custom input[type="submit"]:focus,
.colors-custom .entry-footer .edit-link a.post-edit-link:hover,
.colors-custom .entry-footer .edit-link a.post-edit-link:focus,
.colors-custom .social-navigation a,
.colors-custom .prev.page-numbers:focus,
.colors-custom .prev.page-numbers:hover,
.colors-custom .next.page-numbers:focus,
.colors-custom .next.page-numbers:hover,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item:hover,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item:focus {
background: hsl( ' . esc_attr( $hue ) . ', ' . esc_attr( $saturation ) . ', 46% ); /* base: #767676; */
}
.colors-custom button.secondary:hover,
.colors-custom button.secondary:focus,
.colors-custom input[type="reset"]:hover,
.colors-custom input[type="reset"]:focus,
.colors-custom input[type="button"].secondary:hover,
.colors-custom input[type="button"].secondary:focus,
.colors-custom input[type="reset"].secondary:hover,
.colors-custom input[type="reset"].secondary:focus,
.colors-custom input[type="submit"].secondary:hover,
.colors-custom input[type="submit"].secondary:focus,
.colors-custom hr {
background: hsl( ' . $hue . ', ' . $saturation . ', 73% ); /* base: #bbb; */
}
.colors-custom input[type="text"],
.colors-custom input[type="email"],
.colors-custom input[type="url"],
.colors-custom input[type="password"],
.colors-custom input[type="search"],
.colors-custom input[type="number"],
.colors-custom input[type="tel"],
.colors-custom input[type="range"],
.colors-custom input[type="date"],
.colors-custom input[type="month"],
.colors-custom input[type="week"],
.colors-custom input[type="time"],
.colors-custom input[type="datetime"],
.colors-custom input[type="datetime-local"],
.colors-custom input[type="color"],
.colors-custom textarea,
.colors-custom select,
.colors-custom fieldset,
.colors-custom .widget .tagcloud a:hover,
.colors-custom .widget .tagcloud a:focus,
.colors-custom .widget.widget_tag_cloud a:hover,
.colors-custom .widget.widget_tag_cloud a:focus,
.colors-custom .wp_widget_tag_cloud a:hover,
.colors-custom .wp_widget_tag_cloud a:focus {
border-color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); /* base: #bbb; */
}
.colors-custom thead th {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); /* base: #bbb; */
}
.colors-custom .entry-footer .cat-links .icon,
.colors-custom .entry-footer .tags-links .icon {
color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); /* base: #bbb; */
}
.colors-custom button.secondary,
.colors-custom input[type="reset"],
.colors-custom input[type="button"].secondary,
.colors-custom input[type="reset"].secondary,
.colors-custom input[type="submit"].secondary,
.colors-custom .prev.page-numbers,
.colors-custom .next.page-numbers {
background-color: hsl( ' . $hue . ', ' . $saturation . ', 87% ); /* base: #ddd; */
}
.colors-custom .widget .tagcloud a,
.colors-custom .widget.widget_tag_cloud a,
.colors-custom .wp_widget_tag_cloud a {
border-color: hsl( ' . $hue . ', ' . $saturation . ', 87% ); /* base: #ddd; */
}
.colors-custom.twentyseventeen-front-page article:not(.has-post-thumbnail):not(:first-child),
.colors-custom .widget ul li {
border-top-color: hsl( ' . $hue . ', ' . $saturation . ', 87% ); /* base: #ddd; */
}
.colors-custom .widget ul li {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 87% ); /* base: #ddd; */
}
.colors-custom pre,
.colors-custom mark,
.colors-custom ins {
background: hsl( ' . $hue . ', ' . $saturation . ', 93% ); /* base: #eee; */
}
.colors-custom .navigation-top,
.colors-custom .main-navigation > div > ul,
.colors-custom .pagination,
.colors-custom .comments-pagination,
.colors-custom .entry-footer,
.colors-custom .site-footer {
border-top-color: hsl( ' . $hue . ', ' . $saturation . ', 93% ); /* base: #eee; */
}
.colors-custom .navigation-top,
.colors-custom .main-navigation li,
.colors-custom .entry-footer,
.colors-custom .single-featured-image-header,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item,
.colors-custom tr {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 93% ); /* base: #eee; */
}
.colors-custom .site-content .wp-playlist-light {
border-color: hsl( ' . $hue . ', ' . $saturation . ', 93% ); /* base: #eee; */
}
.colors-custom .site-header,
.colors-custom .single-featured-image-header {
background-color: hsl( ' . $hue . ', ' . $saturation . ', 98% ); /* base: #fafafa; */
}
.colors-custom button,
.colors-custom input[type="button"],
.colors-custom input[type="submit"],
.colors-custom .entry-footer .edit-link a.post-edit-link,
.colors-custom .social-navigation a,
.colors-custom .site-content .wp-playlist-light a.wp-playlist-caption:hover,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item:hover a,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item:focus a,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item:hover,
.colors-custom .site-content .wp-playlist-light .wp-playlist-item:focus,
.colors-custom .prev.page-numbers:focus,
.colors-custom .prev.page-numbers:hover,
.colors-custom .next.page-numbers:focus,
.colors-custom .next.page-numbers:hover,
.colors-custom.has-header-image .site-title,
.colors-custom.has-header-video .site-title,
.colors-custom.has-header-image .site-title a,
.colors-custom.has-header-video .site-title a,
.colors-custom.has-header-image .site-description,
.colors-custom.has-header-video .site-description {
color: hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: #fff; */
}
body.colors-custom,
.colors-custom .navigation-top,
.colors-custom .main-navigation ul {
background: hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: #fff; */
}
.colors-custom .widget ul li a,
.colors-custom .site-footer .widget-area ul li a {
-webkit-box-shadow: inset 0 -1px 0 hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: rgba(255, 255, 255, 1); */
box-shadow: inset 0 -1px 0 hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: rgba(255, 255, 255, 1); */
}
.colors-custom .menu-toggle,
.colors-custom .menu-toggle:hover,
.colors-custom .menu-toggle:focus,
.colors-custom .menu .dropdown-toggle,
.colors-custom .menu-scroll-down,
.colors-custom .menu-scroll-down:hover,
.colors-custom .menu-scroll-down:focus {
background-color: transparent;
}
.colors-custom .widget .tagcloud a,
.colors-custom .widget .tagcloud a:focus,
.colors-custom .widget .tagcloud a:hover,
.colors-custom .widget.widget_tag_cloud a,
.colors-custom .widget.widget_tag_cloud a:focus,
.colors-custom .widget.widget_tag_cloud a:hover,
.colors-custom .wp_widget_tag_cloud a,
.colors-custom .wp_widget_tag_cloud a:focus,
.colors-custom .wp_widget_tag_cloud a:hover,
.colors-custom .entry-footer .edit-link a.post-edit-link:focus,
.colors-custom .entry-footer .edit-link a.post-edit-link:hover {
-webkit-box-shadow: none !important;
box-shadow: none !important;
}
/* Reset non-customizable hover styling for links */
.colors-custom .entry-content a:hover,
.colors-custom .entry-content a:focus,
.colors-custom .entry-summary a:hover,
.colors-custom .entry-summary a:focus,
.colors-custom .comment-content a:focus,
.colors-custom .comment-content a:hover,
.colors-custom .widget a:hover,
.colors-custom .widget a:focus,
.colors-custom .site-footer .widget-area a:hover,
.colors-custom .site-footer .widget-area a:focus,
.colors-custom .posts-navigation a:hover,
.colors-custom .posts-navigation a:focus,
.colors-custom .widget_authors a:hover strong,
.colors-custom .widget_authors a:focus strong {
-webkit-box-shadow: inset 0 0 0 rgba(0, 0, 0, 0), 0 3px 0 rgba(0, 0, 0, 1);
box-shadow: inset 0 0 0 rgba(0, 0, 0, 0), 0 3px 0 rgba(0, 0, 0, 1);
}
.colors-custom .gallery-item a,
.colors-custom .gallery-item a:hover,
.colors-custom .gallery-item a:focus {
-webkit-box-shadow: none;
box-shadow: none;
}
@media screen and (min-width: 48em) {
.colors-custom .nav-links .nav-previous .nav-title .icon,
.colors-custom .nav-links .nav-next .nav-title .icon {
color: hsl( ' . $hue . ', ' . $saturation . ', 20% ); /* base: #222; */
}
.colors-custom .main-navigation li li:hover,
.colors-custom .main-navigation li li.focus {
background: hsl( ' . $hue . ', ' . $saturation . ', 46% ); /* base: #767676; */
}
.colors-custom .navigation-top .menu-scroll-down {
color: hsl( ' . $hue . ', ' . $saturation . ', 46% ); /* base: #767676; */;
}
.colors-custom abbr[title] {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 46% ); /* base: #767676; */;
}
.colors-custom .main-navigation ul ul {
border-color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); /* base: #bbb; */
background: hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: #fff; */
}
.colors-custom .main-navigation ul li.menu-item-has-children:before,
.colors-custom .main-navigation ul li.page_item_has_children:before {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 73% ); /* base: #bbb; */
}
.colors-custom .main-navigation ul li.menu-item-has-children:after,
.colors-custom .main-navigation ul li.page_item_has_children:after {
border-bottom-color: hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: #fff; */
}
.colors-custom .main-navigation li li.focus > a,
.colors-custom .main-navigation li li:focus > a,
.colors-custom .main-navigation li li:hover > a,
.colors-custom .main-navigation li li a:hover,
.colors-custom .main-navigation li li a:focus,
.colors-custom .main-navigation li li.current_page_item a:hover,
.colors-custom .main-navigation li li.current-menu-item a:hover,
.colors-custom .main-navigation li li.current_page_item a:focus,
.colors-custom .main-navigation li li.current-menu-item a:focus {
color: hsl( ' . $hue . ', ' . $saturation . ', 100% ); /* base: #fff; */
}
}
`

View File

@ -1,48 +0,0 @@
package twentyseventeen
import (
"fmt"
"github.com/fthvgb1/wp-go/app/theme/wp"
)
func customHeader(h *wp.Handle) (r string) {
themeMods := h.CommonThemeMods()
headerTextColor := themeMods.HeaderTextcolor
if headerTextColor == "" || headerTextColor == themeMods.ThemeSupport.CustomHeader.DefaultTextColor {
return
}
css := `
.site-title,
.site-description {
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
}`
if headerTextColor != "blank" {
css = fmt.Sprintf(customHeaderCss, headerTextColor)
}
r = fmt.Sprintf(`<style id="twentyseventeen-custom-header-styles" type="text/css">%s</style>`, css)
return
}
var customHeaderCss = `
.site-title a,
.colors-dark .site-title a,
.colors-custom .site-title a,
body.has-header-image .site-title a,
body.has-header-video .site-title a,
body.has-header-image.colors-dark .site-title a,
body.has-header-video.colors-dark .site-title a,
body.has-header-image.colors-custom .site-title a,
body.has-header-video.colors-custom .site-title a,
.site-description,
.colors-dark .site-description,
.colors-custom .site-description,
body.has-header-image .site-description,
body.has-header-video .site-description,
body.has-header-image.colors-dark .site-description,
body.has-header-video.colors-dark .site-description,
body.has-header-image.colors-custom .site-description,
body.has-header-video.colors-custom .site-description {
color: #%s;
}
`

View File

@ -1,5 +0,0 @@
{{define "layout/footer"}}
{{template "common/footer" .}}
{{ block "footer" .}}
{{end}}
{{end}}

View File

@ -1,3 +0,0 @@
{{define "layout/sidebar" }}
{{template "common/sidebarWidget" .}}
{{end}}

View File

@ -1,16 +0,0 @@
{{template "layout/base" .}}
{{define "content"}}
<div class="site-content-contain">
<div id="content" class="site-content">
<div class="wrap">
<div id="primary" class="content-area">
<main id="main" class="site-main">
{{template "layout/empty"}}
</main>
</div>
</div>
</div>
</div>
{{end}}

View File

@ -1,43 +0,0 @@
package twentyseventeen
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
)
func pushScripts(h *wp.Handle) {
h.PushCacheGroupHeadScript(constraints.AllScene, "{theme}.head", 30, func(h *wp.Handle) string {
head := headScript
if "dark" == wpconfig.GetThemeModsVal(ThemeName, "colorscheme", "light") {
head = fmt.Sprintf("%s\n%s", headScript, ` <link rel="stylesheet" id="twentyseventeen-colors-dark-css" href="/wp-content/themes/twentyseventeen/assets/css/colors-dark.css?ver=20191025" media="all">`)
}
return head
})
h.PushGroupFooterScript(constraints.AllScene, "{theme}.footer", 20.005, footerScript)
}
var headScript = `<link rel='stylesheet' id='twentyseventeen-style-css' href='/wp-content/themes/twentyseventeen/style.css?ver=20221101' media='all' />
<link rel='stylesheet' id='twentyseventeen-block-style-css' href='/wp-content/themes/twentyseventeen/assets/css/blocks.css?ver=20220912' media='all' />
<!--[if lt IE 9]>
<link rel='stylesheet' id='twentyseventeen-ie8-css' href='/wp-content/themes/twentyseventeen/assets/css/ie8.css?ver=20161202' media='all' />
<![endif]-->
<!--[if lt IE 9]>
<script src='/wp-content/themes/twentyseventeen/assets/js/html5.js?ver=20161020' id='html5-js'></script>
<![endif]-->
<script src='/wp-includes/js/jquery/jquery.min.js?ver=3.6.0' id='jquery-core-js'></script>
<script src='/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.3.2' id='jquery-migrate-js'></script>
<link rel="https://api.w.org/" href="/wp-json/" /><link rel="EditURI" type="application/rsd+xml" title="RSD" href="/xmlrpc.php?rsd" />
<link rel="wlwmanifest" type="application/wlwmanifest+xml" href="/wp-includes/wlwmanifest.xml" />
<meta name="generator" content="WordPress 6.1.1" />
<style>.recentcomments a{display:inline !important;padding:0 !important;margin:0 !important;}</style>`
var footerScript = `<script id="twentyseventeen-skip-link-focus-fix-js-extra">
var twentyseventeenScreenReaderText = {"quote":"<svg class=\"icon icon-quote-right\" aria-hidden=\"true\" role=\"img\"> <use href=\"#icon-quote-right\" xlink:href=\"#icon-quote-right\"><\/use> <\/svg>"};
</script>
<script src="/wp-content/themes/twentyseventeen/assets/js/skip-link-focus-fix.js?ver=20161114" id="twentyseventeen-skip-link-focus-fix-js"></script>
<script src="/wp-content/themes/twentyseventeen/assets/js/global.js?ver=20211130" id="twentyseventeen-global-js"></script>
<script src="/wp-content/themes/twentyseventeen/assets/js/jquery.scrollTo.js?ver=2.1.3" id="jquery-scrollto-js"></script>`

View File

@ -1,212 +0,0 @@
package twentyseventeen
import "github.com/fthvgb1/wp-go/app/wpconfig"
type themeSupport struct {
CustomLineHeight bool `json:"custom-line-height"`
StarterContent StarterContent `json:"starter-content"`
}
type Widgets struct {
Sidebar1 []string `json:"sidebar-1"`
Sidebar2 []string `json:"sidebar-2"`
Sidebar3 []string `json:"sidebar-3"`
}
type About struct {
Thumbnail string `json:"thumbnail"`
}
type Contact struct {
Thumbnail string `json:"thumbnail"`
}
type Blog struct {
Thumbnail string `json:"thumbnail"`
}
type HomepageSection struct {
Thumbnail string `json:"thumbnail"`
}
type ImageEspresso struct {
PostTitle string `json:"post_title"`
File string `json:"file"`
}
type ImageSandwich struct {
PostTitle string `json:"post_title"`
File string `json:"file"`
}
type ImageCoffee struct {
PostTitle string `json:"post_title"`
File string `json:"file"`
}
type Attachments struct {
ImageEspresso ImageEspresso `json:"image-espresso"`
ImageSandwich ImageSandwich `json:"image-sandwich"`
ImageCoffee ImageCoffee `json:"image-coffee"`
}
type Image struct {
PostTitle string `json:"post_title"`
File string `json:"file"`
}
type Options struct {
ShowOnFront string `json:"show_on_front"`
PageOnFront string `json:"page_on_front"`
PageForPosts string `json:"page_for_posts"`
}
type ThemeMods struct {
Panel1 string `json:"panel_1"`
Panel2 string `json:"panel_2"`
Panel3 string `json:"panel_3"`
Panel4 string `json:"panel_4"`
}
type Menus struct {
Name string `json:"name"`
Items []string `json:"items"`
}
type NavMenus struct {
Top Menus `json:"top"`
Social Menus `json:"social"`
}
type StarterContent struct {
Widgets Widgets `json:"widgets"`
Posts map[string]map[string]string `json:"posts"`
Attachments map[string]Image `json:"attachments"`
Options Options `json:"options"`
ThemeMods ThemeMods `json:"theme_mods"`
NavMenus NavMenus `json:"nav_menus"`
}
var themesupport = themeSupport{
CustomLineHeight: true,
StarterContent: StarterContent{
Widgets: Widgets{
Sidebar1: []string{"text_business_info", "search", "text_about"},
Sidebar2: []string{"text_business_info"},
Sidebar3: []string{"text_about", "search"},
},
Posts: map[string]map[string]string{
"0": {
"home": "home",
},
"about": {
"thumbnail": "{{image-sandwich}}",
},
"contact": {
"thumbnail": "{{image-espresso}}",
},
"blog": {
"thumbnail": "{{image-coffee}}",
},
"homepage-section": {
"thumbnail": "{{image-espresso}}",
},
},
Attachments: map[string]Image{
"image-espresso": {
PostTitle: "浓缩咖啡",
File: "assets/images/espresso.jpg",
},
"image-sandwich": {
PostTitle: "三明治",
File: "assets/images/sandwich.jpg",
},
"image-coffee": {
PostTitle: "咖啡",
File: "assets/images/coffee.jpg",
},
},
Options: Options{
ShowOnFront: "page",
PageOnFront: "{{home}}",
PageForPosts: "{{blog}}",
},
ThemeMods: ThemeMods{
Panel1: "{{homepage-section}}",
Panel2: "{{about}}",
Panel3: "{{blog}}",
Panel4: "{{contact}}",
},
NavMenus: NavMenus{
Top: Menus{
Name: "顶部菜单",
Items: []string{
"link_home",
"page_about",
"page_blog",
"page_contact",
},
},
Social: Menus{
Name: "社交网络链接菜单",
Items: []string{
"link_yelp",
"link_facebook",
"link_twitter",
"link_instagram",
"link_email",
},
},
},
},
}
var _ = func() struct{} {
v := wpconfig.ThemeSupport{
CoreBlockPatterns: true,
WidgetsBlockEditor: true,
AutomaticFeedLinks: true,
TitleTag: true,
PostThumbnails: true,
Menus: true,
HTML5: []string{
"comment-form",
"comment-list",
"gallery",
"caption",
"script",
"style",
"navigation-widgets",
},
PostFormats: []string{
"aside",
"image",
"video",
"quote",
"link",
"gallery",
"audio",
},
CustomLogo: wpconfig.CustomLogo{
Width: 250,
Height: 250,
FlexWidth: true,
FlexHeight: false,
HeaderText: "",
UnlinkHomepageLogo: false,
},
CustomizeSelectiveRefreshWidgets: true,
EditorStyle: true,
EditorStyles: true,
WpBlockStyles: true,
ResponsiveEmbeds: true,
CustomHeader: wpconfig.CustomHeader{
DefaultImage: "http://wp.test/wp-content/themes/twentyseventeen/assets/images/header.jpg",
RandomDefault: false,
Width: 2000,
Height: 1200,
FlexHeight: true,
FlexWidth: false,
DefaultTextColor: "",
HeaderText: true,
Uploads: true,
WpHeadCallback: "twentyseventeen_header_style",
AdminHeadCallback: "",
AdminPreviewCallback: "",
Video: true,
VideoActiveCallback: "is_front_page",
},
Widgets: true,
}
wpconfig.SetThemeSupport(ThemeName, v)
return struct{}{}
}()

View File

@ -1,229 +0,0 @@
package twentyseventeen
import (
"context"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components"
"github.com/fthvgb1/wp-go/app/theme/wp/middleware"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/plugin/pagination"
"strings"
)
const ThemeName = "twentyseventeen"
var paginate = func() plugins.PageEle {
p := plugins.TwentyFifteenPagination()
p.PrevEle = `<a class="prev page-numbers" href="%s"><svg class="icon icon-arrow-left" aria-hidden="true" role="img"> <use href="#icon-arrow-left" xlink:href="#icon-arrow-left"></use> </svg>
<span class="screen-reader-text">上一页</span></a>`
p.NextEle = strings.Replace(p.NextEle, "下一页", `<span class="screen-reader-text">下一页</span>
<svg class="icon icon-arrow-right" aria-hidden="true" role="img"> <use href="#icon-arrow-right" xlink:href="#icon-arrow-right"></use>
</svg>`, 1)
commentPageEle = plugins.PaginationNav{
Currents: p.Current,
Prevs: p.Prev,
Nexts: p.Next,
Dotss: p.Dots,
Middles: p.Middle,
Urlss: plugins.TwentyFifteenCommentPagination().Urls,
}
return p
}()
var commentPageEle pagination.Render
func Hook(h *wp.Handle) {
wp.Run(h, configs)
}
func configs(h *wp.Handle) {
wp.InitPipe(h)
middleware.CommonMiddleware(h)
h.AddActionFilter("bodyClass", calClass)
h.PushCacheGroupHeadScript(constraints.AllScene, "colorScheme-customHeader", 10, colorScheme, customHeader)
components.WidgetArea(h)
pushScripts(h)
h.PushRender(constraints.AllStats, wp.NewHandleFn(calCustomHeader, 10.005, "calCustomHeader"))
wp.SetComponentsArgs(widgets.Widget, map[string]string{
"{$before_widget}": `<section id="%s" class="%s">`,
"{$after_widget}": `</section>`,
})
h.PushRender(constraints.AllStats,
wp.NewHandleFn(wp.PreTemplate, 70.005, "wp.PreTemplate"),
wp.NewHandleFn(errorsHandle, 80.005, "errorsHandle"),
)
videoHeader(h)
h.SetData("colophon", colophon)
setPaginationAndRender(h)
h.CommonComponents()
h.PushPostPlugin(postThumbnail)
wp.SetComponentsArgsForMap(widgets.Search, "{$form}", searchForm)
wp.PushIndexHandler(constraints.PipeRender, h, wp.NewHandleFn(wp.IndexRender, 10.005, "wp.IndexRender"))
h.PushRender(constraints.Detail, wp.NewHandleFn(wp.DetailRender, 10.005, "wp.DetailRender"))
h.PushDataHandler(constraints.Detail, wp.NewHandleFn(wp.Detail, 100.005, "wp.Detail"), wp.NewHandleFn(postThumb, 90.005, "{theme}.postThumb"))
wp.PushIndexHandler(constraints.PipeData, h, wp.NewHandleFn(wp.Index, 100.005, "wp.Index"))
h.PushDataHandler(constraints.AllScene, wp.NewHandleFn(wp.PreCodeAndStats, 90.005, "wp.PreCodeAndStats"))
}
func setPaginationAndRender(h *wp.Handle) {
h.PushHandler(constraints.PipeRender, constraints.Detail, wp.NewHandleFn(func(hh *wp.Handle) {
d := hh.GetDetailHandle()
d.CommentRender = commentFormat
d.CommentPageEle = commentPageEle
d.CommentStep = 2
}, 150, "setPaginationAndRender"))
wp.PushIndexHandler(constraints.PipeRender, h, wp.NewHandleFn(func(hh *wp.Handle) {
i := hh.GetIndexHandle()
i.SetPageEle(paginate)
}, 150, "setPaginationAndRender"))
}
var searchForm = `<form role="search" method="get" class="search-form" action="/">
<label for="search-form-1">
<span class="screen-reader-text">{$label}</span>
</label>
<input type="search" id="search-form-1" class="search-field" placeholder="{$placeholder}…" value="{$value}" name="s">
<button type="submit" class="search-submit">
<svg class="icon icon-search" aria-hidden="true" role="img"> <use href="#icon-search" xlink:href="#icon-search"></use> </svg>
<span class="screen-reader-text">{$button}</span>
</button>
</form>`
func errorsHandle(h *wp.Handle) {
switch h.Stats {
case constraints.Error404, constraints.InternalErr, constraints.ParamError:
logs.IfError(h.Err(), "报错:")
h.SetTempl("twentyseventeen/posts/error.gohtml")
}
}
func postThumb(h *wp.Handle) {
d := h.GetDetailHandle()
if d.Post.Thumbnail.Path != "" {
img := wpconfig.Thumbnail(d.Post.Thumbnail.OriginAttachmentData, "full", "", "thumbnail", "post-thumbnail")
img.Sizes = "100vw"
img.Srcset = fmt.Sprintf("%s %dw, %s", img.Path, img.Width, img.Srcset)
d.Post.Thumbnail = img
}
}
var commentFormat = comment{}
type comment struct {
plugins.CommonCommentFormat
}
var commentLi = plugins.CommonLi()
var respondFn = plugins.Responds(respondStr)
func (c comment) FormatLi(_ context.Context, m models.Comments, depth, maxDepth, page int, isTls, isThreadComments bool, eo, parent string) string {
return plugins.FormatLi(commentLi, m, respondFn, depth, maxDepth, page, isTls, isThreadComments, eo, parent)
}
var colophon = `<footer id="colophon" class="site-footer">
<div class="wrap">
<div class="site-info">
<a href="https://github.com/fthvgb1/wp-go" class="imprint">自豪地采用 wp-go</a>
</div>
</div>
</footer>`
var respondStr = `<a rel="nofollow" class="comment-reply-link"
href="/p/{{PostId}}?replytocom={{CommentId}}#respond" data-commentid="{{CommentId}}" data-postid="{{PostId}}"
data-belowelement="div-comment-{{CommentId}}" data-respondelement="respond"
data-replyto="回复给{{CommentAuthor}}"
aria-label="回复给{{CommentAuthor}}"><svg class="icon icon-mail-reply" aria-hidden="true" role="img"> <use href="#icon-mail-reply" xlink:href="#icon-mail-reply"></use> </svg>回复</a>`
func postThumbnail(h *wp.Handle, posts *models.Posts) {
if posts.Thumbnail.Path != "" {
posts.Thumbnail.Sizes = "(max-width: 767px) 89vw, (max-width: 1000px) 54vw, (max-width: 1071px) 543px, 580px"
if h.Scene() == constraints.Detail {
posts.Thumbnail.Sizes = "100vw"
}
}
}
var header = reload.Vars(models.PostThumbnail{}, "twentyseventeen-headerImage")
func calCustomHeader(h *wp.Handle) {
h.SetData("HeaderImage", getHeaderImage(h))
}
func getHeaderImage(h *wp.Handle) (r models.PostThumbnail) {
img := header.Load()
if img.Path != "" {
return img
}
image, rand := h.GetCustomHeaderImg()
if image.Path != "" {
r = image
r.Sizes = "100vw"
if !rand {
header.Store(r)
}
return
}
r.Path = helper.CutUrlHost(h.CommonThemeMods().ThemeSupport.CustomHeader.DefaultImage)
r.Width = 2000
r.Height = 1200
header.Store(r)
return
}
func calClass(h *wp.Handle, s string, _ ...any) string {
class := strings.Split(s, " ")
themeMods := h.CommonThemeMods()
u := wpconfig.GetThemeModsVal(ThemeName, "header_image", themeMods.ThemeSupport.CustomHeader.DefaultImage)
if u != "" && u != "remove-header" {
class = append(class, "has-header-image")
}
if len(themeMods.SidebarsWidgets.Data.Sidebar1) > 0 {
class = append(class, "has-sidebar")
}
if themeMods.HeaderTextcolor == "blank" {
class = append(class, "title-tagline-hidden")
}
class = append(class, "hfeed")
class = append(class, str.Join("colors-", wpconfig.GetThemeModsVal(ThemeName, "colorscheme", "light")))
if h.Scene() == constraints.Archive {
if "one-column" == wpconfig.GetThemeModsVal(ThemeName, "page_layout", "") {
class = append(class, "page-one-column")
} else {
class = append(class, "page-two-column")
}
}
return strings.Join(class, " ")
}
func videoHeader(h *wp.Handle) {
h.AddActionFilter("videoSetting", videoPlay)
wp.CustomVideo(h, constraints.Home)
}
func videoPlay(h *wp.Handle, _ string, a ...any) string {
if len(a) < 1 {
return ""
}
v, ok := a[0].(*wp.VideoSetting)
if !ok {
return ""
}
img := getHeaderImage(h)
v.Width = img.Width
v.Height = img.Height
v.PosterUrl = img.Path
v.L10n.Play = `<span class="screen-reader-text">播放背景视频</span><svg class="icon icon-play" aria-hidden="true" role="img"> <use href="#icon-play" xlink:href="#icon-play"></use> </svg>`
v.L10n.Pause = `<span class="screen-reader-text">暂停背景视频</span><svg class="icon icon-pause" aria-hidden="true" role="img"> <use href="#icon-pause" xlink:href="#icon-pause"></use> </svg>`
return ""
}

View File

@ -1,131 +0,0 @@
package wp
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper/number"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strconv"
"strings"
)
func bodyClass(h *Handle) func() string {
return func() string {
return h.BodyClass()
}
}
func (h *Handle) BodyClass() string {
var class []string
if constraints.Ok != h.Stats {
class = append(class, "error404")
}
switch h.scene {
case constraints.Home:
class = append(class, "home", "blog")
case constraints.Archive:
class = append(class, "archive", "date")
case constraints.Search:
s := "search-no-results"
if len(h.GetIndexHandle().Posts) > 0 {
s = "search-results"
}
class = append(class, "search", s)
case constraints.Category, constraints.Tag:
class = append(class, "archive", "category")
cat := h.GetIndexHandle().Param.Category
if cat == "" {
break
}
_, cate := slice.SearchFirst(cache.CategoriesTags(h.C, h.scene), func(my models.TermsMy) bool {
return my.Name == cat
})
if cate.Slug[0] != '%' {
class = append(class, str.Join("category-", cate.Slug))
}
class = append(class, str.Join("category-", number.IntToString(cate.Terms.TermId)))
case constraints.Author:
class = append(class, "archive", "author")
author := h.GetIndexHandle().Param.Author
user, _ := cache.GetUserByName(h.C, author)
class = append(class, str.Join("author-", number.IntToString(user.Id)))
if user.DisplayName[0] != '%' {
class = append(class, str.Join("author-", user.DisplayName))
}
case constraints.Detail:
class = append(class, "post-template-default", "single", "single-post")
class = append(class, str.Join("postid-", number.IntToString(h.GetDetailHandle().Post.Id)))
if len(h.themeMods.ThemeSupport.PostFormats) > 0 {
class = append(class, "single-format-standard")
}
}
if wpconfig.IsCustomBackground(h.theme) {
class = append(class, "custom-background")
}
if h.themeMods.CustomLogo > 0 || str.ToInteger(wpconfig.GetOption("site_logo"), 0) > 0 {
class = append(class, "wp-custom-logo")
}
if h.themeMods.ThemeSupport.ResponsiveEmbeds {
class = append(class, "wp-embed-responsive")
}
return h.DoActionFilter("bodyClass", strings.Join(class, " "))
}
func postClass(h *Handle) func(posts models.Posts) string {
return func(posts models.Posts) string {
return h.PostClass(posts)
}
}
func (h *Handle) PostClass(posts models.Posts) string {
var class []string
class = append(class, fmt.Sprintf("post-%d", posts.Id), posts.PostType,
str.Join("type-", posts.PostType), str.Join("status-", posts.PostStatus),
"hentry", "format-standard")
if h.CommonThemeMods().ThemeSupport.PostThumbnails && posts.Thumbnail.Path != "" {
class = append(class, "has-post-thumbnail")
}
if posts.PostPassword != "" {
if h.GetPassword() != posts.PostPassword {
class = append(class, "post-password-required")
} else {
class = append(class, "post-password-projected")
}
}
if h.scene == constraints.Home && h.IsStick(posts.Id) {
class = append(class, "sticky")
}
for _, id := range posts.TermIds {
term, ok := wpconfig.GetTermMy(id)
if !ok || term.Slug == "" {
continue
}
class = append(class, TermClass(term))
}
return h.DoActionFilter("postClass", strings.Join(class, " "))
}
func TermClass(term models.TermsMy) string {
termClass := term.Slug
if strings.Contains(term.Slug, "%") {
termClass = strconv.FormatUint(term.TermTaxonomy.TermId, 10)
}
switch term.Taxonomy {
case "post_tag":
return str.Join("tag-", termClass)
case "post_format":
return fmt.Sprintf("format-%s", strings.ReplaceAll(term.Slug, "post-format-", ""))
}
return str.Join(term.Taxonomy, "-", termClass)
}

View File

@ -1,126 +0,0 @@
package wp
import (
"context"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"time"
)
func RenderComment(ctx context.Context, page int, render plugins.CommentHtml, ids []uint64, timeout time.Duration, isTLS bool) (string, error) {
ca, _ := cachemanager.GetMapCache[uint64, models.Comments]("postCommentData")
children, _ := cachemanager.GetMapCache[uint64, []uint64]("commentChildren")
h := CommentHandle{
maxDepth: str.ToInteger(wpconfig.GetOption("thread_comments_depth"), 5),
depth: 1,
isTls: isTLS,
html: render,
order: wpconfig.GetOption("comment_order"),
ca: ca,
children: children,
threadComments: wpconfig.GetOption("thread_comments") == "1",
page: page,
}
return h.formatComments(ctx, ids, timeout)
}
type CommentHandle struct {
maxDepth int
depth int
isTls bool
html plugins.CommentHtml
order string
page int
ca *cache.MapCache[uint64, models.Comments]
children *cache.MapCache[uint64, []uint64]
threadComments bool
}
func (c CommentHandle) findGrandchildComments(ctx context.Context, timeout time.Duration, comments []models.Comments) ([]models.Comments, error) {
parentIds := slice.Map(comments, func(t models.Comments) uint64 {
return t.CommentId
})
children, err := c.children.GetCacheBatch(ctx, parentIds, timeout)
if err != nil {
return nil, err
}
rr := slice.FilterAndMap(children, func(t []uint64) ([]uint64, bool) {
return t, len(t) > 0
})
if len(rr) < 1 {
return comments, nil
}
ids := slice.Decompress(rr)
r, err := c.ca.GetCacheBatch(ctx, ids, timeout)
if err != nil {
return nil, err
}
rrr, err := c.findGrandchildComments(ctx, timeout, r)
if err != nil {
return nil, err
}
comments = append(comments, rrr...)
slice.Sort(comments, func(i, j models.Comments) bool {
return c.html.FloorOrder(i, j)
})
return comments, nil
}
func (c CommentHandle) formatComments(ctx context.Context, ids []uint64, timeout time.Duration) (html string, err error) {
comments, err := c.ca.GetCacheBatch(ctx, ids, timeout)
if err != nil {
return "", err
}
if c.depth > 1 && c.depth < c.maxDepth {
slice.Sort(comments, func(i, j models.Comments) bool {
return c.html.FloorOrder(i, j)
})
}
fixChildren := false
if c.depth >= c.maxDepth {
comments, err = c.findGrandchildComments(ctx, timeout, comments)
if err != nil {
return "", err
}
fixChildren = true
}
s := str.NewBuilder()
for i, comment := range comments {
eo := "even"
if (i+1)%2 == 0 {
eo = "odd"
}
parent := ""
fl := false
var children []uint64
if !fixChildren {
children, err = c.children.GetCache(ctx, comment.CommentId, timeout)
if err != nil {
return "", err
}
}
if c.threadComments && len(children) > 0 && c.depth < c.maxDepth+1 {
parent = "parent"
fl = true
}
s.WriteString(c.html.FormatLi(ctx, comment, c.depth, c.maxDepth, c.page, c.isTls, c.threadComments, eo, parent))
if fl {
c.depth++
ss, err := c.formatComments(ctx, children, timeout)
if err != nil {
return "", err
}
s.WriteString(`<ol class="children">`, ss, `</ol>`)
c.depth--
}
s.WriteString("</li><!-- #comment-## -->")
}
html = s.String()
return
}

View File

@ -1,256 +0,0 @@
package wp
import (
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"strings"
)
var handleComponents = safety.NewMap[string, map[string][]Components[string]]()
var handleComponentHook = safety.NewMap[string, map[string][]func(Components[string]) (Components[string], bool)]()
var componentsArgs = safety.NewMap[string, any]()
var componentFilterFns = safety.NewMap[string, []func(*Handle, string, ...any) string]()
func (h *Handle) DeleteComponents(scene, componentKey, componentName string) {
v, ok := handleComponentHook.Load(scene)
if !ok {
v = make(map[string][]func(Components[string]) (Components[string], bool))
}
v[componentKey] = append(v[componentKey], func(c Components[string]) (Components[string], bool) {
return c, c.Name != componentName
})
handleComponentHook.Store(scene, v)
}
func (h *Handle) ReplaceComponents(scene, componentKey, componentName string, components Components[string]) {
v, ok := handleComponentHook.Load(scene)
if !ok {
v = make(map[string][]func(Components[string]) (Components[string], bool))
}
v[componentKey] = append(v[componentKey], func(c Components[string]) (Components[string], bool) {
if c.Name == componentName {
c = components
}
return c, true
})
handleComponentHook.Store(scene, v)
}
func (h *Handle) PushComponentHooks(scene, componentKey string, fn func(Components[string]) (Components[string], bool)) {
v, ok := handleComponentHook.Load(scene)
if !ok {
v = make(map[string][]func(Components[string]) (Components[string], bool))
}
v[componentKey] = append(v[componentKey], fn)
handleComponentHook.Store(scene, v)
}
var GetAndHookComponents = reload.BuildMapFnWithAnyParams[string]("calComponents", HookComponent)
func HookComponent(a ...any) []Components[string] {
componentKey := a[0].(string)
scene := a[1].(string)
mut := reload.GetGlobeMutex()
mut.Lock()
defer mut.Unlock()
components := GetComponents(scene, componentKey)
r := slice.FilterAndMap(components, func(component Components[string]) (Components[string], bool) {
keyHooks, ok := handleComponentHook.Load(scene)
if !ok {
return component, true
}
hooks := keyHooks[componentKey]
for _, fn := range hooks {
hookedComponent, ok := fn(component)
if !ok { // DeleteComponents fn
return hookedComponent, false
}
component = hookedComponent // ReplaceComponents fn
}
return component, true
})
slice.SimpleSort(r, slice.DESC, func(t Components[string]) float64 {
return t.Order
})
return r
}
func GetComponents(scene, key string) (r []Components[string]) {
sceneComponents, _ := handleComponents.Load(scene)
allSceneComponents, _ := handleComponents.Load(constraints.AllScene)
r = append(sceneComponents[key], allSceneComponents[key]...)
return
}
var CacheComponent = reload.BuildMapFnWithAnyParams[string]("cacheComponents", cacheComponentFn)
func cacheComponentFn(a ...any) string {
return a[0].(Components[string]).Fn(a[1].(*Handle))
}
func CalComponent(h *Handle) func(string) string {
return func(componentKey string) string {
cacheKey := str.Join("get-hook-Components-", h.scene, "-", componentKey)
hookedComponents := GetAndHookComponents(cacheKey, componentKey, h.scene)
var s = make([]string, 0, len(hookedComponents))
for _, component := range hookedComponents {
if component.Val != "" {
s = append(s, component.Val)
continue
}
if component.Fn != nil {
v := ""
if component.Cached {
key := str.Join(h.scene, "-", componentKey, "-", component.Name)
v = CacheComponent(key, component, h)
} else {
v = component.Fn(h)
}
if v != "" {
s = append(s, v)
}
}
}
return strings.Join(s, "\n")
}
}
func (h *Handle) PushComponents(scene, componentType string, components ...Components[string]) {
c, ok := handleComponents.Load(scene)
if !ok {
c = make(map[string][]Components[string])
}
c[componentType] = append(c[componentType], components...)
handleComponents.Store(scene, c)
}
func (h *Handle) PushGroupComponentStr(scene, componentType, name string, order float64, strs ...string) {
var component = Components[string]{
Val: strings.Join(slice.FilterAndMap(strs, func(t string) (string, bool) {
t = strings.Trim(t, " \n\r\t\v\x00")
if t == "" {
return "", false
}
return t, true
}), "\n"),
Order: order,
Name: name,
}
h.PushComponents(scene, componentType, component)
}
func (h *Handle) PushCacheGroupHeadScript(scene, name string, order float64, fns ...func(*Handle) string) {
h.PushGroupCacheComponentFn(scene, constraints.HeadScript, name, order, fns...)
}
func (h *Handle) PushFooterScript(scene string, components ...Components[string]) {
h.PushComponents(scene, constraints.FooterScript, components...)
}
func (h *Handle) PushGroupFooterScript(scene, name string, order float64, strs ...string) {
h.PushGroupComponentStr(scene, constraints.FooterScript, name, order, strs...)
}
func (h *Handle) PushCacheGroupFooterScript(scene, name string, order float64, fns ...func(*Handle) string) {
h.PushGroupCacheComponentFn(scene, constraints.FooterScript, name, order, fns...)
}
func (h *Handle) PushGroupCacheComponentFn(scene, componentType, name string, order float64, fns ...func(*Handle) string) {
h.PushComponents(scene, componentType, NewComponent(name, "", true, order, func(h *Handle) string {
return strings.Join(slice.Map(fns, func(t func(*Handle) string) string {
return t(h)
}), "\n")
}))
}
func NewComponent(name, val string, cached bool, order float64, fn func(handle *Handle) string) Components[string] {
return Components[string]{Fn: fn, Name: name, Cached: cached, Order: order, Val: val}
}
func (h *Handle) AddCacheComponent(scene, componentType, name string, order float64, fn func(*Handle) string) {
h.PushComponents(scene, componentType, NewComponent(name, "", true, order, fn))
}
func (h *Handle) PushHeadScript(scene string, components ...Components[string]) {
h.PushComponents(scene, constraints.HeadScript, components...)
}
func (h *Handle) PushGroupHeadScript(scene, name string, order float64, str ...string) {
h.PushGroupComponentStr(scene, constraints.HeadScript, name, order, str...)
}
func GetComponentsArgs[T any](k string, defaults T) T {
v, ok := componentsArgs.Load(k)
if ok {
vv, ok := v.(T)
if ok {
return vv
}
}
return defaults
}
func PushComponentsArgsForSlice[T any](name string, v ...T) {
val, ok := componentsArgs.Load(name)
if !ok {
var vv []T
vv = append(vv, v...)
componentsArgs.Store(name, vv)
return
}
vv, ok := val.([]T)
if ok {
vv = append(vv, v...)
componentsArgs.Store(name, vv)
}
}
func SetComponentsArgsForMap[K comparable, V any](name string, key K, v V) {
val, ok := componentsArgs.Load(name)
if !ok {
vv := make(map[K]V)
vv[key] = v
componentsArgs.Store(name, vv)
return
}
vv, ok := val.(map[K]V)
if ok {
vv[key] = v
componentsArgs.Store(name, vv)
}
}
func MergeComponentsArgsForMap[K comparable, V any](name string, m map[K]V) {
val, ok := componentsArgs.Load(name)
if !ok {
componentsArgs.Store(name, m)
return
}
vv, ok := val.(map[K]V)
if ok {
componentsArgs.Store(name, maps.Merge(vv, m))
}
}
func SetComponentsArgs(key string, value any) {
componentsArgs.Store(key, value)
}
func (h *Handle) GetComponentFilterFn(name string) ([]func(*Handle, string, ...any) string, bool) {
return componentFilterFns.Load(name)
}
func (h *Handle) AddActionFilter(name string, fns ...func(*Handle, string, ...any) string) {
v, _ := componentFilterFns.Load(name)
v = append(v, fns...)
componentFilterFns.Store(name, v)
}
func (h *Handle) DoActionFilter(name, s string, args ...any) string {
calls, ok := componentFilterFns.Load(name)
if ok {
return slice.Reduce(calls, func(fn func(*Handle, string, ...any) string, r string) string {
return fn(h, r, args...)
}, s)
}
return s
}

View File

@ -1,42 +0,0 @@
package components
import (
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components/block"
"github.com/fthvgb1/wp-go/app/wpconfig"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
var blockFn = map[string]func(*wp.Handle, string, block.ParserBlock) (func() string, error){
"core/categories": block.Category,
}
func Block(id string) (func(*wp.Handle) string, string) {
content := wpconfig.GetPHPArrayVal("widget_block", "", str.ToInteger[int64](id, 0), "content")
if content == "" {
return nil, ""
}
var name string
v := block.ParseBlock(content)
if len(v.Output) > 0 {
name = v.Output[0].Name
}
return func(h *wp.Handle) string {
var out []string
for _, parserBlock := range v.Output {
fn, ok := blockFn[parserBlock.Name]
if ok {
s, err := fn(h, id, parserBlock)
if err != nil {
logs.Error(err, str.Join("parse block", parserBlock.Name, " fail "), parserBlock)
continue
}
out = append(out, s())
}
}
return strings.Join(out, "\n")
}, name
}

View File

@ -1,96 +0,0 @@
package block
import (
"github.com/dlclark/regexp2"
str "github.com/fthvgb1/wp-go/helper/strings"
)
type Block struct {
Name string
Attrs string
Len int
StartOffset int
Type string
}
type BockParser struct {
Document string
Offset int
Output []ParserBlock
}
type ParserBlock struct {
Name string
Attrs string
InnerBlocks string
InnerHtml string
InnerContent string
}
var block = regexp2.MustCompile(`<!--\s+(?<closer>/)?wp:(?<namespace>[a-z][a-z0-9_-]*\/)?(?<name>[a-z][a-z0-9_-]*)\s+(?<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*)?}\s+)?(?<void>\/)?-->`, regexp2.IgnoreCase|regexp2.Singleline)
func ParseBlock(content string) (r BockParser) {
m, err := block.FindStringMatch(content)
if err != nil {
panic(err)
}
r.Document = content
for m != nil {
if m.GroupCount() < 1 {
continue
}
b := token(m.Groups())
bb := ParserBlock{}
bb.Name = b.Name
bb.Attrs = b.Attrs
r.Output = append(r.Output, bb)
m, _ = block.FindNextMatch(m)
}
return
}
func token(g []regexp2.Group) (b Block) {
if len(g) < 1 {
b.Type = "no-more-tokens"
return
}
var closer, name, void, nameSpace = "", "", "", ""
for i, group := range g {
v := group.String()
if v == "" {
continue
}
switch i {
case 0:
b.Len = group.Length
b.StartOffset = group.Index
case 1:
closer = v
case 2:
nameSpace = v
case 3:
name = v
case 4:
b.Attrs = v
case 5:
void = v
default:
continue
}
}
if nameSpace == "" {
nameSpace = "core/"
}
b.Name = str.Join(nameSpace, name)
if void != "" {
b.Type = "void-block"
return
}
if closer != "" {
b.Type = "block-closer"
return
}
b.Type = "block-opener"
return b
}

View File

@ -1,25 +0,0 @@
package block
import "testing"
func TestParseBlock(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
}{
{
name: "t1",
args: args{
s: `<!-- wp:categories {"showPostCounts":true,"showEmpty":true} /-->`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ParseBlock(tt.args.s)
})
}
}

View File

@ -1,196 +0,0 @@
package block
import (
"encoding/json"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
constraints2 "github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components/widget"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/number"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
func categoryConf() map[any]any {
return map[any]any{
"count": int64(0),
"dropdown": int64(0),
"hierarchical": int64(0),
"title": "分类",
}
}
func categoryDefaultArgs() map[string]string {
return map[string]string{
"{$before_widget}": `<aside id="%s" class="%s">`,
"{$after_widget}": `</aside>`,
"{$name}": "cat",
"{$class}": "postform",
"{$selectId}": "cat",
"{$required}": "",
"{$show_option_none}": "选择分类",
"{$title}": "",
}
}
func parseAttr(attr map[any]any) string {
var attrs []string
class := maps.GetAnyAnyValWithDefaults(attr, "", "className")
classes := strings.Split(class, " ")
fontsize := maps.GetAnyAnyValWithDefaults(attr, "", "fontSize")
if fontsize != "" {
classes = append(classes, fmt.Sprintf("has-%s-font-size", fontsize))
}
style := maps.GetAnyAnyValWithDefaults[map[any]any](attr, nil, "style", "typography")
if len(style) > 0 {
styless := maps.AnyAnyMapTo(style, func(k, v any) (string, string, bool) {
kk, ok := k.(string)
if !ok {
return "", "", false
}
vv, ok := v.(string)
if !ok {
return "", "", false
}
return kk, vv, true
})
styles := maps.FilterToSlice(styless, func(k string, v string) (string, bool) {
k = str.CamelCaseTo(k, '-')
return str.Join(k, ":", v), true
})
attrs = append(attrs, fmt.Sprintf(`style="%s;"`, strings.Join(styles, ";")))
}
attrs = append(attrs, fmt.Sprintf(`class="%s"`, strings.Join(classes, " ")))
return strings.Join(attrs, " ")
}
var GetCategoryAttr = reload.BuildValFn("block-category-attr", parseAttr)
var GetCategoryConf = reload.BuildValFnWithConfirm("block-category-conf", categoryConfFn, 5)
func categoryConfFn(blockParser ParserBlock) (map[any]any, bool) {
var con any
err := json.Unmarshal([]byte(blockParser.Attrs), &con)
if err != nil {
logs.Error(err, "解析category attr错误", blockParser.Attrs)
return nil, false
}
var conf map[any]any
switch con.(type) {
case map[any]any:
conf = con.(map[any]any)
case map[string]any:
conf = maps.StrAnyToAnyAny(con.(map[string]any))
}
conf = maps.FilterZeroMerge(categoryConf(), conf)
if maps.GetAnyAnyValWithDefaults(conf, false, "showPostCounts") {
conf["count"] = int64(1)
}
if maps.GetAnyAnyValWithDefaults(conf, false, "displayAsDropdown") {
conf["dropdown"] = int64(1)
}
if maps.GetAnyAnyValWithDefaults(conf, false, "showHierarchy") {
conf["hierarchical"] = int64(1)
}
class := maps.GetAnyAnyValWithDefaults(conf, "", "className")
classes := strings.Split(class, " ")
classes = append(classes, "wp-block-categories")
if conf["dropdown"].(int64) == 1 {
classes = append(classes, "wp-block-categories-dropdown")
conf["className"] = strings.Join(classes, " ")
} else {
classes = append(classes, "wp-block-categories-list")
conf["className"] = strings.Join(classes, " ")
}
return conf, true
}
var GetCategoryArgs = reload.BuildValFnWithAnyParams("block-category-args", categoryArgs)
func categoryArgs(_ ...any) map[string]string {
args := wp.GetComponentsArgs(widgets.Widget, map[string]string{})
return maps.FilterZeroMerge(categoryDefaultArgs(), args)
}
func Category(h *wp.Handle, id string, blockParser ParserBlock) (func() string, error) {
counter := number.Counters[int]()
var err error
conf := GetCategoryConf(blockParser)
if err != nil {
return nil, err
}
if conf == nil {
return nil, errors.New("解析block-category配置错误")
}
if maps.GetAnyAnyValWithDefaults(conf, false, "showEmpty") {
h.C.Set("showEmpty", true)
}
if maps.GetAnyAnyValWithDefaults(conf, false, "showOnlyTopLevel") {
h.C.Set("showOnlyTopLevel", true)
}
args := GetCategoryArgs()
return func() string {
return category(h, id, counter, args, conf)
}, nil
}
func category(h *wp.Handle, id string, counter number.Counter[int], args map[string]string, conf map[any]any) string {
var out = ""
categories := cache.CategoriesTags(h.C, constraints2.Category)
class := []string{"widget", "widget_block", "widget_categories"}
if conf["dropdown"].(int64) == 1 {
out = dropdown(h, categories, counter(), args, conf)
} else {
out = categoryUl(h, categories, conf)
}
before := fmt.Sprintf(args["{$before_widget}"], str.Join("block-", id), strings.Join(class, " "))
return str.Join(before, out, args["{$after_widget}"])
}
func categoryUl(h *wp.Handle, categories []models.TermsMy, confAttr map[any]any) string {
s := str.NewBuilder()
li := widget.CategoryLi(h, confAttr, categories)
attrs := GetCategoryAttr(confAttr)
s.Sprintf(`<ul %s>%s</ul>`, attrs, li)
return s.String()
}
func dropdown(h *wp.Handle, categories []models.TermsMy, id int, args map[string]string, confAttr map[any]any) string {
s := str.NewBuilder()
ids := fmt.Sprintf(`wp-block-categories-%v`, id)
args = maps.Copy(args)
args["{$selectId}"] = ids
attrs := GetCategoryAttr(confAttr)
selects := widget.DropdownCategories(h, args, confAttr, categories)
s.Sprintf(`<div %s><label class="screen-reader-text" for="%s">%s</label>%s%s</div>`, attrs, ids, args["{$title}"], selects, strings.ReplaceAll(categoryDropdownScript, "{$id}", ids))
return s.String()
}
var categoryDropdownScript = `
<script type='text/javascript'>
/* <![CDATA[ */
( function() {
const dropdown = document.getElementById( '{$id}' );
function onCatChange() {
if ( dropdown.options[ dropdown.selectedIndex ].value > 0 ) {
location.href = "/?cat=" + dropdown.options[ dropdown.selectedIndex ].value;
}
}
dropdown.onchange = onCatChange;
})();
/* ]]> */
</script>
`

View File

@ -1,128 +0,0 @@
package widget
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
var archiveTemplate = `{$before_widget}
{$title}
{$nav}
{$html}
{$navCloser}
{$after_widget}
`
func archiveArgs() map[string]string {
return map[string]string{
"{$before_sidebar}": "",
"{$after_sidebar}": "",
"{$nav}": "",
"{$navCloser}": "",
"{$title}": "",
"{$dropdown_id}": "archives-dropdown-2",
"{$dropdown_type}": "monthly",
"{$dropdown_label}": "选择月份",
}
}
var archivesConfig = map[any]any{
"count": int64(0),
"dropdown": int64(0),
"title": "归档",
}
var GetArchiveConf = BuildconfigFn(archivesConfig, "widget_archives", int64(2))
var GetArchiveArgs = reload.BuildValFnWithAnyParams("widget_archive-args", archiveArgsFn)
func archiveArgsFn(a ...any) map[string]string {
h := a[0].(*wp.Handle)
conf := a[1].(map[any]any)
id := a[2].(string)
archiveArgs := archiveArgs()
commonArgs := wp.GetComponentsArgs(widgets.Widget, CommonArgs())
args := wp.GetComponentsArgs(widgets.Archive, archiveArgs)
args = maps.FilterZeroMerge(archiveArgs, CommonArgs(), commonArgs, args)
args["{$before_widget}"] = fmt.Sprintf(args["{$before_widget}"], str.Join("archives-", id), str.Join("widget widget_", "archive"))
args["{$title}"] = str.Join(args["{$before_title}"], conf["title"].(string), args["{$after_title}"])
if conf["dropdown"].(int64) == 0 && slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "navigation-widgets") {
args["{$nav}"] = fmt.Sprintf(`<nav aria-label="%s">`, conf["title"].(string))
args["{$navCloser}"] = "</nav>"
}
return args
}
func Archive(h *wp.Handle, id string) string {
conf := GetArchiveConf()
args := GetArchiveArgs(h, conf, id)
s := archiveTemplate
if int64(1) == conf["dropdown"].(int64) {
s = strings.ReplaceAll(s, "{$html}", archiveDropDown(h, conf, args, cache.Archives(h.C)))
} else {
s = strings.ReplaceAll(s, "{$html}", archiveUl(h, conf, args, cache.Archives(h.C)))
}
return h.DoActionFilter(widgets.Archive, str.Replace(s, args))
}
var dropdownScript = `
<script>
/* <![CDATA[ */
(function() {
const dropdown = document.getElementById("archives-dropdown-2");
function onSelectChange() {
if ( dropdown.options[ dropdown.selectedIndex ].value !== '' ) {
document.location.href = this.options[ this.selectedIndex ].value;
}
}
dropdown.onchange = onSelectChange;
})();
/* ]]> */
</script>`
func archiveDropDown(h *wp.Handle, conf map[any]any, args map[string]string, archives []models.PostArchive) string {
option := str.NewBuilder()
option.Sprintf(`<option value="">%s</option>`, args["{$dropdown_label}"])
i := h.GetIndexHandle()
month := strings.TrimLeft(i.Param.Month, "0")
showCount := conf["count"].(int64)
for _, archive := range archives {
sel := ""
if i.Param.Year == archive.Year && month == archive.Month {
sel = "selected"
}
count := ""
if showCount == int64(1) {
count = fmt.Sprintf("(%v)", archive.Posts)
}
option.Sprintf(`<option %s value="/p/date/%s/%02s" >%s年%s月 %s</option>
`, sel, archive.Year, archive.Month, archive.Year, archive.Month, count)
}
s := str.NewBuilder()
s.Sprintf(`<label class="screen-reader-text" for="%s">%s</label>
<select id="%s" name="archive-dropdown">%s</select>%s
`, args["{$dropdown_id}"], args["{$title}"], args["{$dropdown_id}"], option.String(), dropdownScript)
return s.String()
}
func archiveUl(h *wp.Handle, conf map[any]any, args map[string]string, archives []models.PostArchive) string {
s := str.NewBuilder()
s.WriteString(`<ul>`)
showCount := conf["count"].(int64)
for _, archive := range archives {
count := ""
if showCount == 1 {
count = fmt.Sprintf("(%v)", archive.Posts)
}
s.Sprintf(`<li><a href="/p/date/%[1]s/%02[2]s">%[1]s年%[2]s月%[3]s</a></li>`, archive.Year, archive.Month, count)
}
return s.String()
}

View File

@ -1,10 +0,0 @@
package widget
func CommonArgs() map[string]string {
return map[string]string{
"{$before_widget}": `<aside id="%s" class="%s">`,
"{$after_widget}": "</aside>",
"{$before_title}": `<h2 class="widget-title">`,
"{$after_title}": "</h2>",
}
}

View File

@ -1,277 +0,0 @@
package widget
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/helper/tree"
"strings"
)
var categoryTemplate = `{$before_widget}
{$title}
{$nav}
{$html}
{$navCloser}
{$after_widget}
`
var categoryConfig = map[any]any{
"count": int64(0),
"dropdown": int64(0),
"hierarchical": int64(0),
"title": "分类",
}
func categoryArgs() map[string]string {
return map[string]string{
"{$before_sidebar}": "",
"{$after_sidebar}": "",
"{$class}": "postform",
"{$show_option_none}": "选择分类",
"{$name}": "cat",
"{$selectId}": "cat",
"{$required}": "",
"{$nav}": "",
"{$navCloser}": "",
"{$title}": "",
}
}
var GetCategoryConf = BuildconfigFn(categoryConfig, "widget_categories", int64(2))
var GetCategoryArgs = reload.BuildValFnWithAnyParams("widget-category-args", categoryArgsFn)
func categoryArgsFn(a ...any) map[string]string {
h := a[0].(*wp.Handle)
conf := a[1].(map[any]any)
id := a[2].(string)
commonArgs := wp.GetComponentsArgs(widgets.Widget, map[string]string{})
args := wp.GetComponentsArgs(widgets.Categories, categoryArgs())
args = maps.FilterZeroMerge(categoryArgs(), CommonArgs(), commonArgs, args)
args["{$before_widget}"] = fmt.Sprintf(args["{$before_widget}"], str.Join("categories-", id), str.Join("widget widget_", "categories"))
args["{$title}"] = str.Join(args["{$before_title}"], conf["title"].(string), args["{$after_title}"])
if conf["dropdown"].(int64) == 0 && slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "navigation-widgets") {
args["{$nav}"] = fmt.Sprintf(`<nav aria-label="%s">`, args["{title}"])
args["{$navCloser}"] = "</nav>"
}
return args
}
func Category(h *wp.Handle, id string) string {
conf := GetCategoryConf()
args := GetCategoryArgs(h, conf, id)
t := categoryTemplate
dropdown := conf["dropdown"].(int64)
categories := cache.CategoriesTags(h.C, constraints.Category)
if dropdown == 1 {
t = strings.ReplaceAll(t, "{$html}", CategoryDropdown(h, args, conf, categories))
} else {
t = strings.ReplaceAll(t, "{$html}", categoryUL(h, args, conf, categories))
}
return h.DoActionFilter(widgets.Categories, str.Replace(t, args))
}
func CategoryLi(h *wp.Handle, conf map[any]any, categories []models.TermsMy) string {
s := str.NewBuilder()
isCount := conf["count"].(int64)
currentCate := models.TermsMy{}
if h.Scene() == constraints.Category {
cat := h.C.Param("category")
_, currentCate = slice.SearchFirst(categories, func(my models.TermsMy) bool {
return cat == my.Name
})
}
if conf["hierarchical"].(int64) == 0 {
for _, category := range categories {
count := ""
if isCount != 0 {
count = fmt.Sprintf("(%d)", category.Count)
}
current := ""
if category.TermTaxonomyId == currentCate.TermTaxonomyId {
current = "current-cat"
}
s.Sprintf(` <li class="cat-item cat-item-%d %s">
<a href="/p/category/%s">%s %s</a>
</li>
`, category.Terms.TermId, current, category.Name, category.Name, count)
}
} else {
m := tree.Roots(categories, 0, func(cate models.TermsMy) (child, parent uint64) {
return cate.TermTaxonomyId, cate.Parent
})
cate := &tree.Node[models.TermsMy, uint64]{Data: models.TermsMy{}}
if currentCate.TermTaxonomyId > 0 {
cate = m[currentCate.TermTaxonomyId]
}
r := m[0]
categoryLi(r, cate, tree.Ancestor(m, 0, cate), isCount, s)
}
return s.String()
}
func categoryUL(h *wp.Handle, args map[string]string, conf map[any]any, categories []models.TermsMy) string {
s := str.NewBuilder()
s.WriteString("<ul>\n")
s.WriteString(CategoryLi(h, conf, categories))
s.WriteString("</ul>")
return s.String()
}
func categoryLi(root *tree.Node[models.TermsMy, uint64], cate, roots *tree.Node[models.TermsMy, uint64], isCount int64, s *str.Builder) {
for _, child := range *root.Children {
category := child.Data
count := ""
if isCount != 0 {
count = fmt.Sprintf("(%d)", category.Count)
}
var class []string
if len(*child.Children) > 0 && cate.Data.TermTaxonomyId > 0 {
if category.TermTaxonomyId == cate.Parent {
class = append(class, "current-cat-parent")
}
if cate.Parent > 0 && category.TermTaxonomyId == roots.Data.TermTaxonomyId {
class = append(class, "current-cat-ancestor")
}
}
aria := ""
if category.TermTaxonomyId == cate.Data.TermTaxonomyId {
class = append(class, "current-cat")
aria = `aria-current="page"`
}
s.Sprintf(` <li class="cat-item cat-item-%d %s">
<a %s href="/p/category/%s">%s %s</a>
`, category.Terms.TermId, strings.Join(class, " "), aria, category.Name, category.Name, count)
if len(*child.Children) > 0 {
s.WriteString(` <ul class="children">
`)
categoryLi(&child, cate, roots, isCount, s)
s.WriteString(`</ul>
`)
}
s.Sprintf(`</li>`)
}
}
var categoryDropdownJs = `/* <![CDATA[ */
(function() {
var dropdown = document.getElementById( "%s" );
function onCatChange() {
if ( dropdown.options[ dropdown.selectedIndex ].value > 0 ) {
dropdown.parentNode.submit();
}
}
dropdown.onchange = onCatChange;
})();
/* ]]> */
`
func CategoryDropdown(h *wp.Handle, args map[string]string, conf map[any]any, categories []models.TermsMy) string {
s := str.NewBuilder()
s.WriteString(`<form action="/" method="get">
`)
s.Sprintf(` <label class="screen-reader-text" for="%s">%s</label>
`, args["{$selectId}"], args["{$title}"])
s.WriteString(DropdownCategories(h, args, conf, categories))
s.WriteString("</form>\n")
attr := ""
if !slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "script") {
attr = ` type="text/javascript"`
}
s.Sprintf(`<script%s>
`, attr)
s.Sprintf(categoryDropdownJs, args["{$selectId}"])
s.WriteString("</script>\n")
return s.String()
}
func DropdownCategories(h *wp.Handle, args map[string]string, conf map[any]any, categories []models.TermsMy) string {
if len(categories) < 1 {
return ""
}
s := str.NewBuilder()
s.Sprintf(` <select %s name="%s" id="%s" class="%s">
`, args["{$required}"], args["{$name}"], args["{$selectId}"], args["{$class}"])
s.Sprintf(` <option value="-1">%s</option>
`, args["{$show_option_none}"])
currentCategory := ""
i := h.GetIndexHandle()
if h.Scene() == constraints.Category {
currentCategory = i.Param.Category
}
showCount := conf["count"].(int64)
fn := func(category models.TermsMy, deep int) {
lv := fmt.Sprintf("level-%d", deep+1)
sep := strings.Repeat("&nbsp;", deep*2)
selected := ""
if category.Name == currentCategory {
selected = "selected"
}
count := ""
if showCount != 0 {
count = fmt.Sprintf("(%d)", category.Count)
}
s.Sprintf(` <option class="%s" %s value="%d">%s%s %s</option>
`, lv, selected, category.Terms.TermId, sep, category.Name, count)
}
if conf["hierarchical"].(int64) == 0 {
for _, category := range categories {
fn(category, 0)
}
} else {
tree.Root(categories, 0, func(t models.TermsMy) (child, parent uint64) {
return t.TermTaxonomyId, t.Parent
}).Loop(func(category models.TermsMy, deep int) {
fn(category, deep)
})
}
s.WriteString(" </select>\n")
return h.DoActionFilter("wp_dropdown_cats", s.String())
}
func IsCategory(h *wp.Handle) (category models.TermsMy, r bool) {
cate := wp.GetComponentsArgs[map[string]string](widgets.Categories, categoryArgs())
name, ok := cate["{$name}"]
if !ok || name == "" {
return
}
cat := h.C.Query(name)
if cat == "" {
return
}
id := str.ToInteger[uint64](cat, 0)
if id < 1 {
return
}
i, cc := slice.SearchFirst(cache.CategoriesTags(h.C, constraints.Category), func(my models.TermsMy) bool {
return id == my.Terms.TermId
})
if i < 0 {
return
}
r = true
category = cc
return
}
func CategoryQueryName(h *wp.Handle) string {
cate := wp.GetComponentsArgs[map[string]string](widgets.Categories, categoryArgs())
name, ok := cate["{$name}"]
if ok {
return name
}
return ""
}

View File

@ -1,26 +0,0 @@
package widget
import (
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
str "github.com/fthvgb1/wp-go/helper/strings"
)
func Fn(id string, fn func(*wp.Handle, string) string) func(h *wp.Handle) string {
return func(h *wp.Handle) string {
return fn(h, id)
}
}
func configFns[K comparable, V any](m map[K]V, key string, a ...any) func(_ ...any) map[K]V {
return func(_ ...any) map[K]V {
c := wpconfig.GetPHPArrayVal[map[K]V](key, nil, a...)
return maps.FilterZeroMerge(maps.Copy(m), c)
}
}
func BuildconfigFn[K comparable, V any](m map[K]V, key string, a ...any) func(_ ...any) map[K]V {
return reload.BuildValFnWithAnyParams(str.Join("widget-config-", key), configFns(m, key, a...))
}

View File

@ -1,66 +0,0 @@
package widget
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
var metaTemplate = `{$before_widget}
{$h2title}
{$nav}
<ul>
{$li}
</ul>
{$navCloser}
{$after_widget}`
func defaultMetaArgs() map[string]string {
return map[string]string{
"{$aria_label}": "",
"{$title}": "",
}
}
var GetMetaArgs = reload.BuildValFnWithAnyParams("widget-meta-args", ParseMetaArgs)
func ParseMetaArgs(a ...any) map[string]string {
h := a[0].(*wp.Handle)
id := a[1].(string)
commonArgs := wp.GetComponentsArgs(widgets.Widget, map[string]string{})
metaArgs := defaultMetaArgs()
args := wp.GetComponentsArgs(widgets.Meta, metaArgs)
args = maps.FilterZeroMerge(metaArgs, CommonArgs(), commonArgs, args)
args["{$before_widget}"] = fmt.Sprintf(args["{$before_widget}"], str.Join("meta-", id), str.Join("widget widget_", "meta"))
args["{$title}"] = wpconfig.GetPHPArrayVal("widget_meta", "其它操作", int64(2), "title")
if args["{$title}"] == "" {
args["{$title}"] = "其他操作"
}
if args["{$title}"] != "" {
args["{$h2title}"] = str.Join(args["{$before_title}"], args["{$title}"], args["{$after_title}"])
}
if slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "navigation-widgets") {
args["{$nav}"] = fmt.Sprintf(`<nav aria-label="%s">`, args["{$title}"])
args["{$navCloser}"] = "</nav>"
}
return args
}
func Meta(h *wp.Handle, id string) string {
args := GetMetaArgs(h, id)
ss := str.NewBuilder()
if str.ToInteger(wpconfig.GetOption("users_can_register"), 0) > 0 {
ss.Sprintf(`<li><a href="/wp-login.php?action=register">注册</li>`)
}
ss.Sprintf(`<li><a href="%s">登录</a></li>`, "/wp-login.php")
ss.Sprintf(`<li><a href="%s">条目feed</a></li>`, "/feed")
ss.Sprintf(`<li><a href="%s">评论feed</a></li>`, "/comments/feed")
s := strings.ReplaceAll(metaTemplate, "{$li}", ss.String())
return h.DoActionFilter(widgets.Meta, str.Replace(s, args))
}

View File

@ -1,75 +0,0 @@
package widget
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
func recentCommentsArgs() map[string]string {
return map[string]string{
"{$before_sidebar}": "",
"{$after_sidebar}": "",
"{$nav}": "",
"{$navCloser}": "",
"{$title}": "",
"{$recent_comments_id}": "recentcomments",
}
}
var recentCommentConf = map[any]any{
"number": int64(5),
"title": "近期评论",
}
var recentCommentsTemplate = `{$before_widget}
{$nav}
{$title}
<ul id="{$recent_comments_id}">
{$li}
</ul>
{$navCloser}
{$after_widget}
`
var GetRecentCommentConf = BuildconfigFn(recentCommentConf, "widget_recent-comments", int64(2))
var GetRecentCommentArgs = reload.BuildValFnWithAnyParams("widget-recent-comment-args", RecentCommentArgs)
func RecentCommentArgs(a ...any) map[string]string {
h := a[0].(*wp.Handle)
conf := a[1].(map[any]any)
id := a[2].(string)
commentsArgs := recentCommentsArgs()
commonArgs := wp.GetComponentsArgs(widgets.Widget, map[string]string{})
args := wp.GetComponentsArgs(widgets.RecentComments, commentsArgs)
args = maps.FilterZeroMerge(commentsArgs, CommonArgs(), commonArgs, args)
args["{$before_widget}"] = fmt.Sprintf(args["{$before_widget}"], str.Join("recent-comments-", id), str.Join("widget widget_", "recent_comments"))
args["{$title}"] = str.Join(args["{$before_title}"], conf["title"].(string), args["{$after_title}"])
if slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "navigation-widgets") {
args["{$nav}"] = fmt.Sprintf(`<nav aria-label="%s">`, conf["title"])
args["{$navCloser}"] = "</nav>"
}
return args
}
func RecentComments(h *wp.Handle, id string) string {
conf := GetRecentCommentConf()
args := GetRecentCommentArgs(h, conf, id)
comments := slice.Map(cache.RecentComments(h.C, int(conf["number"].(int64))), func(t models.Comments) string {
return fmt.Sprintf(` <li>
<span class="comment-author-link">%s</span>发表在
<a href="%s">%s</a>
</li>`, t.CommentAuthor, t.CommentAuthorUrl, t.PostTitle)
})
s := strings.ReplaceAll(recentCommentsTemplate, "{$li}", strings.Join(comments, "\n"))
return h.DoActionFilter(widgets.RecentComments, str.Replace(s, args))
}

View File

@ -1,98 +0,0 @@
package widget
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
var recentPostsTemplate = `{$before_widget}
{$nav}
{$title}
<ul>
{$li}
</ul>
{$navCloser}
{$after_widget}
`
func DefaultRecentPostsArgs() map[string]string {
return map[string]string{
"{$before_sidebar}": "",
"{$after_sidebar}": "",
"{$nav}": "",
"{$navCloser}": "",
"{$title}": "",
}
}
func DefaultRecentConf() map[any]any {
return map[any]any{
"number": int64(5),
"show_date": false,
"title": "近期文章",
}
}
var GetRecentPostConf = reload.BuildValFnWithAnyParams("widget-recent-posts-conf", RecentPostConf)
func RecentPostConf(_ ...any) map[any]any {
recent := DefaultRecentConf()
conf := wpconfig.GetPHPArrayVal[map[any]any]("widget_recent-posts", recent, int64(2))
conf = maps.FilterZeroMerge(recent, conf)
return conf
}
var GetRecentPostArgs = reload.BuildValFnWithAnyParams("widget-recent-posts-args", ParseRecentPostArgs)
func ParseRecentPostArgs(a ...any) map[string]string {
h := a[0].(*wp.Handle)
conf := a[1].(map[any]any)
id := a[2].(string)
recent := DefaultRecentPostsArgs()
commonArgs := wp.GetComponentsArgs(widgets.Widget, map[string]string{})
args := wp.GetComponentsArgs(widgets.RecentPosts, recent)
args = maps.FilterZeroMerge(recent, CommonArgs(), commonArgs, args)
args["{$before_widget}"] = fmt.Sprintf(args["{$before_widget}"], str.Join("recent-posts-", id), str.Join("widget widget_", "recent_entries"))
args["{$title}"] = str.Join(args["{$before_title}"], conf["title"].(string), args["{$after_title}"])
if slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "navigation-widgets") {
args["{$nav}"] = fmt.Sprintf(`<nav aria-label="%s">`, conf["title"])
args["{$navCloser}"] = "</nav>"
}
return args
}
func RecentPosts(h *wp.Handle, id string) string {
conf := GetRecentPostConf()
args := GetRecentPostArgs(h, conf, id)
currentPostId := uint64(0)
if h.Scene() == constraints.Detail {
currentPostId = str.ToInteger(h.C.Param("id"), uint64(0))
}
posts := slice.Map(cache.RecentPosts(h.C, int(conf["number"].(int64))), func(t models.Posts) string {
t = wp.ProjectTitle(t)
date := ""
if v, ok := conf["show_date"].(bool); ok && v {
date = fmt.Sprintf(`<span class="post-date">%s</span>`, t.PostDateGmt.Format("2006年01月02日"))
}
ariaCurrent := ""
if currentPostId == t.Id {
ariaCurrent = ` aria-current="page"`
}
return fmt.Sprintf(` <li>
<a href="/p/%v"%s>%s</a>
%s
</li>`, t.Id, ariaCurrent, t.PostTitle, date)
})
s := strings.ReplaceAll(recentPostsTemplate, "{$li}", strings.Join(posts, "\n"))
return h.DoActionFilter(widgets.RecentPosts, str.Replace(s, args))
}

View File

@ -1,87 +0,0 @@
package widget
import (
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/constraints/widgets"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/html"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
var searchTemplate = `{$before_widget}
{$title}
{$form}
{$after_widget}`
var html5SearchForm = `<form role="search" {$aria_label} method="get" class="search-form" action="/">
<label>
<span class="screen-reader-text">{$label}</span>
<input type="search" class="search-field" placeholder="{$placeholder}" value="{$value}" name="s" />
</label>
<input type="submit" class="search-submit" value="{$button}" />
</form>`
var xmlSearchForm = `<form role="search" {$aria_label} method="get" id="searchform" class="searchform" action="/">
<div>
<label class="screen-reader-text" for="s">{$label}</label>
<input type="text" value="{$value}" name="s" id="s" />
<input type="submit" id="searchsubmit" value="{$button}" />
</div>
</form>`
func searchArgs() map[string]string {
return map[string]string{
"{$aria_label}": "",
"{$title}": "",
"{$form}": "",
"{$button}": "搜索",
"{$placeholder}": "搜索&hellip;",
"{$label}": "搜索:",
}
}
var form = html5SearchForm
var GetSearchArgs = reload.BuildValFnWithAnyParams("widget-search-args", ParseSearchArgs)
func ParseSearchArgs(a ...any) map[string]string {
h := a[0].(*wp.Handle)
id := a[1].(string)
search := searchArgs()
commonArgs := wp.GetComponentsArgs(widgets.Widget, map[string]string{})
args := wp.GetComponentsArgs(widgets.Search, search)
args = maps.FilterZeroMerge(search, CommonArgs(), commonArgs, args)
args["{$before_widget}"] = fmt.Sprintf(args["{$before_widget}"], str.Join("search-", id), str.Join("widget widget_", "search"))
if args["{$title}"] == "" {
args["{$title}"] = wpconfig.GetPHPArrayVal("widget_search", "", int64(2), "title")
}
if args["{$title}"] != "" {
args["{$title}"] = str.Join(args["{$before_title}"], args["{$title}"], args["{$after_title}"])
}
if args["{$form}"] != "" {
form = args["{$form}"]
delete(args, "{$form}")
}
if !slice.IsContained(h.CommonThemeMods().ThemeSupport.HTML5, "navigation-widgets") {
form = xmlSearchForm
}
return args
}
func Search(h *wp.Handle, id string) string {
args := GetSearchArgs(h, id)
s := strings.ReplaceAll(searchTemplate, "{$form}", form)
val := ""
if h.Scene() == constraints.Search {
val = html.SpecialChars(h.GetIndexHandle().Param.Search)
}
s = strings.ReplaceAll(s, "{$value}", val)
return h.DoActionFilter(widgets.Search, str.Replace(s, args))
}

View File

@ -1,61 +0,0 @@
package components
import (
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components/widget"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"strings"
)
var widgetFn = map[string]widgetComponent{
"search": {fn: widget.Search, name: "widget.search"},
"recent-posts": {fn: widget.RecentPosts, name: "widget.recent-posts"},
"recent-comments": {fn: widget.RecentComments, name: "widget.recent-comments"},
"archives": {fn: widget.Archive, name: "widget.archives"},
"categories": {fn: widget.Category, name: "widget.categories"},
"meta": {fn: widget.Meta, name: "widget.meta", cached: true},
}
type widgetComponent struct {
fn func(h *wp.Handle, id string) string
cached bool
name string
}
func WidgetArea(h *wp.Handle) {
h.PushComponents(constraints.AllScene, constraints.SidebarsWidgets, sidebars()...)
}
func sidebars() []wp.Components[string] {
v := wpconfig.GetPHPArrayVal("sidebars_widgets", []any{}, "sidebar-1")
var i = 10.5
return slice.FilterAndMap(v, func(t any) (wp.Components[string], bool) {
vv := t.(string)
ss := strings.Split(vv, "-")
id := ss[len(ss)-1]
name := strings.Join(ss[0:len(ss)-1], "-")
widgetComponents, ok := widgetFn[name]
if name != "block" && !ok {
return wp.Components[string]{}, false
}
var component wp.Components[string]
if name == "block" {
fn, fnName := Block(id)
if fn == nil {
return component, false
}
component.Fn = fn
component.Name = str.Join("block.", fnName)
} else {
component.Fn = widget.Fn(id, widgetComponents.fn)
component.Name = widgetComponents.name
component.Cached = widgetComponents.cached
}
i -= 0.001
component.Order = i
return component, true
})
}

View File

@ -1,239 +0,0 @@
package wp
import (
"encoding/json"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/model"
"regexp"
"strings"
)
func (h *Handle) DisplayHeaderText() bool {
return h.themeMods.ThemeSupport.CustomHeader.HeaderText && "blank" != h.themeMods.HeaderTextcolor
}
var GetCustomHeaderImgFn = reload.BuildValFnWithConfirm("headerImages", customHeadImag, 5)
func customHeadImag(h *Handle) ([]models.PostThumbnail, bool) {
hs, err := h.GetHeaderImages(h.theme)
if err != nil {
h.SetErr(fmt.Errorf("get customheadimage err: %v", err), Low)
return nil, false
}
return hs, true
}
func (h *Handle) GetCustomHeaderImg() (r models.PostThumbnail, isRand bool) {
var err error
img := GetCustomHeaderImgFn(h)
err = h.Err()
if err != nil && strings.Contains(err.Error(), "get customheadimage err") {
logs.Error(err, "获取页眉背景图失败")
return
}
hs := slice.Copy(img)
if len(hs) < 1 {
return
}
if len(hs) > 1 {
isRand = true
}
r, _ = slice.RandPop(&hs)
return
}
type VideoPlay struct {
Pause string `json:"pause,omitempty"`
Play string `json:"play,omitempty"`
PauseSpeak string `json:"pauseSpeak,omitempty"`
PlaySpeak string `json:"playSpeak,omitempty"`
}
type VideoSetting struct {
MimeType string `json:"mimeType,omitempty"`
PosterUrl string `json:"posterUrl,omitempty"`
VideoUrl string `json:"videoUrl,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
MinWidth int `json:"minWidth,omitempty"`
MinHeight int `json:"minHeight,omitempty"`
L10n VideoPlay `json:"l10n"`
}
var videoReg = regexp.MustCompile(`^https?://(?:www\.)?(?:youtube\.com/watch|youtu\.be/)`)
func GetVideoSetting(h *Handle, u string) (string, error) {
img, _ := h.GetCustomHeaderImg()
v := VideoSetting{
MimeType: GetMimeType(u),
PosterUrl: img.Path,
VideoUrl: u,
Width: img.Width,
Height: img.Height,
MinWidth: 900,
MinHeight: 500,
L10n: VideoPlay{
Pause: "暂停",
Play: "播放",
PauseSpeak: "视频已暂停",
PlaySpeak: "视频正在播放",
},
}
if is := videoReg.FindString(u); is != "" {
v.MimeType = "video/x-youtube"
}
_ = h.DoActionFilter("videoSetting", "", &v)
s, err := json.Marshal(v)
if err != nil {
return "", err
}
setting := fmt.Sprintf(`var %s = %s`, "_wpCustomHeaderSettings", string(s))
script := str.Join(`<script id="wp-custom-header-js-extra">`, setting, "</script>\n")
return script, nil
}
func CustomVideo(h *Handle, scene ...string) (ok bool) {
mod, err := wpconfig.GetThemeMods(h.theme)
if err != nil {
logs.Error(err, "getThemeMods fail", h.theme)
return
}
if !mod.ThemeSupport.CustomHeader.Video || (mod.HeaderVideo < 1 && mod.ExternalHeaderVideo == "") {
return
}
u := ""
if mod.HeaderVideo > 0 {
post, err := cache.GetPostById(h.C, uint64(mod.HeaderVideo))
if err != nil {
logs.Error(err, "get headerVideo fail", mod.HeaderVideo)
return
}
u = post.Metas["_wp_attached_file"].(string)
u = str.Join("/wp-content/uploads/", u)
} else {
u = mod.ExternalHeaderVideo
}
hs, err := GetVideoSetting(h, u)
if err != nil {
logs.Error(err, "get headerVideo fail", mod.HeaderVideo)
return
}
scripts := []string{
"/wp-includes/js/dist/vendor/wp-polyfill-inert.min.js",
"/wp-includes/js/dist/vendor/regenerator-runtime.min.js",
"/wp-includes/js/dist/vendor/wp-polyfill.min.js",
"/wp-includes/js/dist/dom-ready.min.js",
"/wp-includes/js/dist/hooks.min.js",
"/wp-includes/js/dist/i18n.min.js",
"/wp-includes/js/dist/a11y.min.js",
"/wp-includes/js/wp-custom-header.min.js",
}
scripts = slice.Map(scripts, func(t string) string {
return fmt.Sprintf(`<script src="%s" id="wp-%s-js"></script>
`, t, str.Replaces(t, []string{
"/wp-includes/js/dist/vendor/",
"/wp-includes/js/dist/",
"/wp-includes/js/",
".min.js",
".js",
"wp-",
"",
}))
})
var tr = `<script id="wp-i18n-js-after">
wp.i18n.setLocaleData( { 'text direction\u0004ltr': [ 'ltr' ] } );
</script>
<script id='wp-a11y-js-translations'>
( function( domain, translations ) {
var localeData = translations.locale_data[ domain ] || translations.locale_data.messages;
localeData[""].domain = domain;
wp.i18n.setLocaleData( localeData, domain );
} )( "default", {"translation-revision-date":"2023-04-23 22:48:55+0000","generator":"GlotPress/4.0.0-alpha.4","domain":"messages","locale_data":{"messages":{"":{"domain":"messages","plural-forms":"nplurals=1; plural=0;","lang":"zh_CN"},"Notifications":["u901au77e5"]}},"comment":{"reference":"wp-includes/js/dist/a11y.js"}} );
</script>
<script src='/wp-includes/js/dist/a11y.min.js?ver=ecce20f002eda4c19664' id='wp-a11y-js'></script>
`
c := []Components[string]{
NewComponent("wp-a11y-js-translations", tr, true, 10.0065, nil),
NewComponent("VideoSetting", hs, true, 10.0064, nil),
NewComponent("header-script", scripts[len(scripts)-1], true, 10.0063, nil),
}
for _, s := range scene {
h.PushGroupFooterScript(s, "wp-custom-header", 10.0066, scripts[0:len(scripts)-2]...)
h.PushFooterScript(s, c...)
}
ok = true
return
}
func (h *Handle) GetHeaderImages(theme string) (r []models.PostThumbnail, err error) {
meta, err := wpconfig.GetThemeMods(theme)
if err != nil || meta.HeaderImage == "" {
return
}
if "random-uploaded-image" != meta.HeaderImage {
m, er := cache.GetPostById(h.C, uint64(meta.HeaderImagData.AttachmentId))
if er != nil {
err = er
return
}
m.Thumbnail = thumb(m, theme)
r = []models.PostThumbnail{m.Thumbnail}
return
}
headers, er := model.Finds[models.Posts](h.C, model.Conditions(
model.Where(model.SqlBuilder{
{"post_type", "attachment"},
{"post_status", "inherit"},
{"meta_value", theme},
{"meta_key", "_wp_attachment_is_custom_header"},
}),
model.Fields("a.ID"),
model.Group("a.ID"),
model.Join(model.SqlBuilder{
{" a", "left join", "wp_postmeta b", "a.ID=b.post_id"},
}),
))
if er != nil {
err = er
return
}
if len(headers) > 0 {
posts, er := cache.GetPostsByIds(h.C, slice.Map(headers, func(t models.Posts) uint64 {
return t.Id
}))
if er != nil {
err = er
return
}
r = slice.Map(posts, func(m models.Posts) models.PostThumbnail {
return thumb(m, theme)
})
}
return
}
func thumb(m models.Posts, theme string) models.PostThumbnail {
m.Thumbnail = wpconfig.Thumbnail(m.AttachmentMetadata, "full", "", "thumbnail", "post-thumbnail", fmt.Sprintf("%s-thumbnail-avatar", theme))
m.Thumbnail.Width = m.AttachmentMetadata.Width
m.Thumbnail.Height = m.AttachmentMetadata.Height
if m.Thumbnail.Path != "" {
if len(m.AttachmentMetadata.Sizes) > 0 {
m.Thumbnail.Srcset = str.Join(m.Thumbnail.Path, " 2000w, ", m.Thumbnail.Srcset)
}
}
return m.Thumbnail
}

View File

@ -1,211 +0,0 @@
package wp
import (
"errors"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/app/plugins/wpposts"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/cachemanager"
"github.com/fthvgb1/wp-go/helper/number"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/plugin/pagination"
"strings"
"time"
)
type DetailHandle struct {
*Handle
CommentRender plugins.CommentHtml
Comments []uint64
Page int
Limit int
Post models.Posts
CommentPageEle pagination.Render
TotalRaw int
TotalPage int
CommentStep int
}
func NewDetailHandle(handle *Handle) *DetailHandle {
return &DetailHandle{
Handle: handle,
Page: 1,
Limit: 5,
CommentStep: 1,
}
}
func (d *DetailHandle) BuildDetailData() (err error) {
d.ginH["title"] = wpconfig.GetOption("blogname")
err = d.CheckAndGetPost()
if err != nil {
return
}
d.CommentData()
d.ContextPost()
return
}
func (d *DetailHandle) CheckAndGetPost() (err error) {
id := str.ToInteger[uint64](d.C.Param("id"), 0)
maxId, err := cache.GetMaxPostId(d.C)
logs.IfError(err, "get max post id")
if id > maxId || id <= 0 {
d.Stats = constraints.ParamError
err = errors.New("无效的文档id")
}
if err != nil {
return
}
post, err := cache.GetPostById(d.C, id)
if post.Id == 0 || err != nil || post.PostStatus != "publish" {
d.Stats = constraints.Error404
logs.IfError(err, "获取id失败")
err = errors.New(str.Join("无效的文档id "))
return
}
d.Post = post
d.ginH["user"] = cache.GetUserById(d.C, post.PostAuthor)
d.ginH["title"] = fmt.Sprintf("%s-%s", post.PostTitle, wpconfig.GetOption("blogname"))
return
}
func (d *DetailHandle) PasswordProject() {
if d.Post.PostPassword != "" {
wpposts.PasswordProjectTitle(&d.Post)
if d.GetPassword() != d.Post.PostPassword {
wpposts.PasswdProjectContent(&d.Post)
}
}
}
func (h *Handle) GetDetailHandle() *DetailHandle {
v, ok := h.C.Get("detailHandle")
if !ok {
vv := NewDetailHandle(h)
h.C.Set("detailHandle", vv)
return vv
}
return v.(*DetailHandle)
}
func (d *DetailHandle) CommentData() {
d.ginH["totalCommentNum"] = 0
d.ginH["totalCommentPage"] = 1
d.ginH["commentPageNav"] = ""
order := wpconfig.GetOption("comment_order")
d.ginH["commentOrder"] = order
d.Limit = str.ToInteger(wpconfig.GetOption("comments_per_page"), 5)
pageComments := wpconfig.GetOption("page_comments")
num, err := cachemanager.GetBy[int]("commentNumber", d.C, d.Post.Id, time.Second)
if err != nil {
d.SetErr(err, Low)
return
}
if num < 1 {
return
}
topNum, err := cachemanager.GetBy[int]("postTopCommentsNum", d.C, d.Post.Id, time.Second)
if err != nil {
d.SetErr(err, Low)
return
}
d.TotalPage = number.DivideCeil(topNum, d.Limit)
if !strings.Contains(d.C.Request.URL.Path, "comment-page") {
defaultCommentsPage := wpconfig.GetOption("default_comments_page")
if order == "desc" && defaultCommentsPage == "oldest" || order == "asc" && defaultCommentsPage == "newest" {
d.C.AddParam("page", number.IntToString(d.TotalPage))
}
}
d.Page = str.ToInteger(d.C.Param("page"), 1)
d.ginH["currentPage"] = d.Page
var key string
if pageComments != "1" {
key = number.IntToString(d.Post.Id)
d.Limit = 0
} else {
key = fmt.Sprintf("%d-%d-%d", d.Post.Id, d.Page, d.Limit)
}
d.ginH["page_comments"] = pageComments
d.ginH["totalCommentPage"] = d.TotalPage
if d.TotalPage < d.Page {
d.SetErr(errors.New("curren page above total page"), High)
return
}
data, err := cache.PostTopLevelCommentIds(d.C, d.Post.Id, d.Page, d.Limit, topNum, order, key)
if err != nil {
d.SetErr(err, Low)
return
}
d.TotalRaw = topNum
d.ginH["totalCommentNum"] = num
d.Comments = data
}
func (d *DetailHandle) RenderComment() {
if d.CommentRender == nil {
d.CommentRender = plugins.CommentRender()
}
ableComment := true
if d.Post.CommentStatus != "open" ||
(d.Post.PostPassword != "" && d.GetPassword() != d.Post.PostPassword) {
ableComment = false
}
d.ginH["showComment"] = ableComment
d.ginH["comments"] = ""
if len(d.Comments) < 0 || !ableComment {
return
}
var err error
d.ginH["comments"], err = RenderComment(d.C, d.Page, d.CommentRender, d.Comments, 2*time.Second, d.IsHttps())
if err != nil {
d.SetErr(err, High)
return
}
if d.CommentPageEle == nil {
d.CommentPageEle = plugins.TwentyFifteenCommentPagination()
}
if wpconfig.GetOption("page_comments") == "1" && d.TotalPage > 1 {
d.ginH["commentPageNav"] = pagination.Paginate(d.CommentPageEle, d.TotalRaw, d.Limit, d.Page, d.CommentStep, *d.C.Request.URL, d.IsHttps())
}
}
func (d *DetailHandle) ContextPost() {
prev, next, err := cache.GetContextPost(d.C, d.Post.Id, d.Post.PostDate)
logs.IfError(err, "get pre and next post", d.Post.Id, d.Post.PostDate)
d.ginH["next"] = next
d.ginH["prev"] = prev
}
func DetailRender(h *Handle) {
if h.Stats != constraints.Ok {
return
}
d := h.GetDetailHandle()
d.PasswordProject()
d.RenderComment()
d.ginH["post"] = d.Post
}
func Detail(h *Handle) {
d := h.GetDetailHandle()
err := d.BuildDetailData()
if err != nil {
d.SetErr(err, High)
}
h.SetData("scene", h.Scene())
}
func ReplyCommentJs(h *Handle) {
h.PushFooterScript(constraints.Detail, NewComponent("comment-reply.js", "", false, 10, func(h *Handle) string {
reply := ""
if h.GetDetailHandle().Post.CommentStatus == "open" && wpconfig.GetOption("thread_comments") == "1" {
reply = `<script src='/wp-includes/js/comment-reply.min.js' id='comment-reply-js'></script>`
}
return reply
}))
}

View File

@ -1,8 +0,0 @@
package wp
const (
None = iota
Low
High
Fatal
)

View File

@ -1,121 +0,0 @@
package wp
import (
"path/filepath"
"regexp"
)
var exts = map[string]string{
"jpg|jpeg|jpe": "image/jpeg",
"gif": "image/gif",
"png": "image/png",
"bmp": "image/bmp",
"tiff|tif": "image/tiff",
"webp": "image/webp",
"ico": "image/x-icon",
"heic": "image/heic",
"asf|asx": "video/x-ms-asf",
"wmv": "video/x-ms-wmv",
"wmx": "video/x-ms-wmx",
"wm": "video/x-ms-wm",
"avi": "video/avi",
"divx": "video/divx",
"flv": "video/x-flv",
"mov|qt": "video/quicktime",
"mpeg|mpg|mpe": "video/mpeg",
"mp4|m4v": "video/mp4",
"ogv": "video/ogg",
"webm": "video/webm",
"mkv": "video/x-matroska",
"3gp|3gpp": "video/3gpp",
"3g2|3gp2": "video/3gpp2",
"txt|asc|c|cc|h|srt": "text/plain",
"csv": "text/csv",
"tsv": "text/tab-separated-values",
"ics": "text/calendar",
"rtx": "text/richtext",
"css": "text/css",
"htm|html": "text/html",
"vtt": "text/vtt",
"dfxp": "application/ttaf+xml",
"mp3|m4a|m4b": "audio/mpeg",
"aac": "audio/aac",
"ra|ram": "audio/x-realaudio",
"wav": "audio/wav",
"ogg|oga": "audio/ogg",
"flac": "audio/flac",
"mid|midi": "audio/midi",
"wma": "audio/x-ms-wma",
"wax": "audio/x-ms-wax",
"mka": "audio/x-matroska",
"rtf": "application/rtf",
"js": "application/javascript",
"pdf": "application/pdf",
"swf": "application/x-shockwave-flash",
"class": "application/java",
"tar": "application/x-tar",
"zip": "application/zip",
"gz|gzip": "application/x-gzip",
"rar": "application/rar",
"7z": "application/x-7z-compressed",
"exe": "application/x-msdownload",
"psd": "application/octet-stream",
"xcf": "application/octet-stream",
"doc": "application/msword",
"pot|pps|ppt": "application/vnd.ms-powerpoint",
"wri": "application/vnd.ms-write",
"xla|xls|xlt|xlw": "application/vnd.ms-excel",
"mdb": "application/vnd.ms-access",
"mpp": "application/vnd.ms-project",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"docm": "application/vnd.ms-word.document.macroEnabled.12",
"dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
"dotm": "application/vnd.ms-word.template.macroEnabled.12",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
"xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
"xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
"xltm": "application/vnd.ms-excel.template.macroEnabled.12",
"xlam": "application/vnd.ms-excel.addin.macroEnabled.12",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
"ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
"ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
"potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
"potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
"ppam": "application/vnd.ms-powerpoint.addin.macroEnabled.12",
"sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
"sldm": "application/vnd.ms-powerpoint.slide.macroEnabled.12",
"onetoc|onetoc2|onetmp|onepkg": "application/onenote",
"oxps": "application/oxps",
"xps": "application/vnd.ms-xpsdocument",
"odt": "application/vnd.oasis.opendocument.text",
"odp": "application/vnd.oasis.opendocument.presentation",
"ods": "application/vnd.oasis.opendocument.spreadsheet",
"odg": "application/vnd.oasis.opendocument.graphics",
"odc": "application/vnd.oasis.opendocument.chart",
"odb": "application/vnd.oasis.opendocument.database",
"odf": "application/vnd.oasis.opendocument.formula",
"wp|wpd": "application/wordperfect",
"key": "application/vnd.apple.keynote",
"numbers": "application/vnd.apple.numbers",
"pages": "application/vnd.apple.pages",
}
var regs = func() map[string]*regexp.Regexp {
var r = make(map[string]*regexp.Regexp)
for k, v := range exts {
r[v] = regexp.MustCompile(`(?i:\.(` + k + `)$)`)
}
return r
}()
func GetMimeType(file string) string {
ext := filepath.Ext(file)
for mime, reg := range regs {
if reg.FindString(ext) != "" {
return mime
}
}
return ""
}

View File

@ -1,74 +0,0 @@
package wp
import (
"errors"
"github.com/fthvgb1/wp-go/safety"
)
var fnMap = safety.NewMap[string, map[string]any]()
var fnHook = safety.NewMap[string, map[string]any]()
func GetFn[T any](fnType string, name string) []T {
v, ok := fnMap.Load(fnType)
if !ok {
return nil
}
vv, ok := v[name]
if !ok {
return nil
}
return vv.([]T)
}
func GetFnHook[T any](fnType string, name string) []T {
v, ok := fnHook.Load(fnType)
if !ok {
return nil
}
vv, ok := v[name]
if !ok {
return nil
}
return vv.([]T)
}
func PushFn[T any](fnType string, name string, fns ...T) error {
v, ok := fnMap.Load(fnType)
if !ok {
v = make(map[string]any)
fnMap.Store(fnType, v)
v[name] = fns
return nil
}
vv, ok := v[name]
if !ok || vv == nil {
v[name] = fns
return nil
}
s, ok := vv.([]T)
if ok {
s = append(s, fns...)
v[name] = s
}
return errors.New("error fn type")
}
func PushFnHook[T any](fnType string, name string, fns ...T) error {
v, ok := fnHook.Load(fnType)
if !ok {
v = make(map[string]any)
fnHook.Store(fnType, v)
v[name] = fns
return nil
}
vv, ok := v[name]
if !ok || vv == nil {
v[name] = fns
return nil
}
s, ok := vv.([]T)
if ok {
s = append(s, fns...)
v[name] = s
}
return errors.New("error fn type")
}

View File

@ -1,191 +0,0 @@
package wp
import (
"database/sql"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/number"
"github.com/fthvgb1/wp-go/helper/slice"
"github.com/fthvgb1/wp-go/model"
"github.com/fthvgb1/wp-go/plugin/pagination"
"strings"
)
type IndexHandle struct {
*Handle
Param *IndexParams
Posts []models.Posts
pageEle pagination.Render
TotalRows int
postsPlugin PostsPlugin
}
func (h *Handle) GetIndexHandle() *IndexHandle {
v, ok := h.C.Get("indexHandle")
if !ok {
vv := NewIndexHandle(h)
h.C.Set("indexHandle", vv)
return vv
}
return v.(*IndexHandle)
}
func (i *IndexHandle) ListPlugin() func(*Handle, *models.Posts) {
return i.postsPlugin
}
func (i *IndexHandle) SetListPlugin(listPlugin func(*Handle, *models.Posts)) {
i.postsPlugin = listPlugin
}
func (i *IndexHandle) PageEle() pagination.Render {
return i.pageEle
}
func (i *IndexHandle) SetPageEle(pageEle pagination.Render) {
i.pageEle = pageEle
}
func NewIndexHandle(handle *Handle) *IndexHandle {
return &IndexHandle{Handle: handle}
}
func PushIndexHandler(pipeScene string, h *Handle, call HandleCall) {
h.PushHandlers(pipeScene, call, constraints.Home,
constraints.Category, constraints.Search, constraints.Tag,
constraints.Archive, constraints.Author,
)
}
func (i *IndexHandle) ParseIndex(parm *IndexParams) (err error) {
i.Param = parm
switch i.scene {
case constraints.Search:
i.Param.ParseSearch()
case constraints.Category:
err = i.Param.ParseCategory()
case constraints.Tag:
err = i.Param.ParseTag()
case constraints.Archive:
err = i.Param.ParseArchive()
case constraints.Author:
err = i.Param.ParseAuthor()
}
if err != nil {
i.Stats = constraints.ParamError
return
}
i.Param.ParseParams()
i.Param.CacheKey = i.Param.getSearchKey()
i.ginH["title"] = i.Param.getTitle()
i.ginH["search"] = i.Param.Search
i.ginH["header"] = i.Param.Header
return
}
func (i *IndexHandle) GetIndexData() (posts []models.Posts, totalRaw int, err error) {
q := &model.QueryCondition{
Where: i.Param.Where,
Order: model.SqlBuilder{{i.Param.OrderBy, i.Param.Order}},
Join: i.Param.Join,
In: [][]any{i.Param.PostType, i.Param.PostStatus},
}
switch i.scene {
case constraints.Home, constraints.Category, constraints.Tag, constraints.Author:
posts, totalRaw, err = cache.PostLists(i.C, i.Param.CacheKey, q, i.Param.Page, i.Param.PageSize)
if i.scene == constraints.Home && i.Param.Page == 1 {
i.MarkSticky(&posts)
}
case constraints.Search:
posts, totalRaw, err = cache.SearchPost(i.C, i.Param.CacheKey, q, i.Param.Page, i.Param.PageSize)
case constraints.Archive:
i.ginH["archiveYear"] = i.Param.Year
i.ginH["archiveMonth"] = strings.TrimLeft(i.Param.Month, "0")
posts, totalRaw, err = cache.GetMonthPostIds(i.C, i.Param.Year, i.Param.Month, i.Param.Page, i.Param.PageSize, i.Param.Order)
}
return
}
func (i *IndexHandle) Pagination() {
if i.pageEle == nil {
i.pageEle = plugins.TwentyFifteenPagination()
}
q := i.C.Request.URL.Query().Encode()
if q != "" {
q = fmt.Sprintf("?%s", q)
}
i.ginH["pagination"] = pagination.Paginate(i.pageEle, i.TotalRows, i.Param.PageSize, i.Param.Page, i.Param.PaginationStep, *i.C.Request.URL, i.IsHttps())
}
func (i *IndexHandle) BuildIndexData() (err error) {
if i.Param == nil {
i.Param = NewIndexParams(i.C)
}
err = i.ParseIndex(i.Param)
if err != nil {
i.Stats = constraints.ParamError
return
}
posts, totalRows, err := i.GetIndexData()
if err != nil && !errors.Is(err, sql.ErrNoRows) {
i.Stats = constraints.Error404
return
}
i.Posts = posts
i.TotalRows = totalRows
i.ginH["totalPage"] = number.DivideCeil(totalRows, i.Param.PageSize)
return
}
var GetPostsPlugin = reload.BuildValFnWithAnyParams("postPlugins", UsePostsPlugins)
func (i *IndexHandle) ExecPostsPlugin() {
fn := i.postsPlugin
if fn == nil {
fn = GetPostsPlugin()
}
for j := range i.Posts {
fn(i.Handle, &i.Posts[j])
}
}
func IndexRender(h *Handle) {
i := h.GetIndexHandle()
i.ExecPostsPlugin()
i.Pagination()
i.ginH["posts"] = i.Posts
}
func Index(h *Handle) {
i := h.GetIndexHandle()
err := i.BuildIndexData()
if err != nil {
i.SetErr(err, High)
}
h.SetData("scene", h.Scene())
}
func (i *IndexHandle) MarkSticky(posts *[]models.Posts) {
a := GetStickPosts(i.Handle)
if len(a) < 1 {
return
}
m := GetStickMapPosts(i.Handle)
*posts = append(a, slice.Filter(*posts, func(post models.Posts, _ int) bool {
_, ok := m[post.Id]
return !ok
})...)
}

View File

@ -1,101 +0,0 @@
package wp
import (
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/plugins"
"github.com/fthvgb1/wp-go/app/plugins/wpposts"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
)
type PostsPlugin func(*Handle, *models.Posts)
func PostsPlugins(initial PostsPlugin, calls ...func(PostsPlugin, *Handle, *models.Posts)) PostsPlugin {
return slice.ReverseReduce(calls, func(t func(PostsPlugin, *Handle, *models.Posts), r PostsPlugin) PostsPlugin {
return func(handle *Handle, posts *models.Posts) {
t(r, handle, posts)
}
}, initial)
}
var pluginFns = reload.Vars(map[string]func(PostsPlugin, *Handle, *models.Posts){
"passwordProject": PasswordProject,
"digest": Digest,
}, "list-post-plugins-fns")
func (h *Handle) PushPostsPlugin(name string, fn func(PostsPlugin, *Handle, *models.Posts)) {
m := pluginFns.Load()
m[name] = fn
}
// PasswordProject 标题和内容密码保护
func PasswordProject(next PostsPlugin, h *Handle, post *models.Posts) {
r := post
if post.PostPassword != "" {
wpposts.PasswordProjectTitle(r)
if h.GetPassword() != post.PostPassword {
wpposts.PasswdProjectContent(r)
return
}
}
next(h, r)
}
// Digest 生成摘要
func Digest(next PostsPlugin, h *Handle, post *models.Posts) {
if post.PostExcerpt != "" {
plugins.PostExcerpt(post)
} else {
plugins.Digest(h.C, post, config.GetConfig().DigestWordCount)
}
next(h, post)
}
var ordinaryPlugin = reload.Vars([]PostsPlugin{}, "ordinaryPlugin")
func (h *Handle) PushPostPlugin(plugin ...PostsPlugin) {
p := ordinaryPlugin.Load()
p = append(p, plugin...)
ordinaryPlugin.Store(p)
}
func PostPlugin(calls ...PostsPlugin) PostsPlugin {
return func(h *Handle, posts *models.Posts) {
for _, call := range calls {
call(h, posts)
}
}
}
func UsePostsPlugins(_ ...any) PostsPlugin {
m := pluginFns.Load()
pluginss := slice.FilterAndMap(config.GetConfig().ListPagePlugins, func(t string) (func(PostsPlugin, *Handle, *models.Posts), bool) {
f, ok := m[t]
return f, ok
})
slice.Unshift(&pluginss, PasswordProject)
return PostsPlugins(PostPlugin(ordinaryPlugin.Load()...), pluginss...)
}
func ListPostPlugins() map[string]func(PostsPlugin, *Handle, *models.Posts) {
return maps.Copy(pluginFns.Load())
}
func ProjectTitle(t models.Posts) models.Posts {
if t.PostPassword != "" {
wpposts.PasswordProjectTitle(&t)
}
return t
}
func GetListPostPlugins(name []string, m map[string]func(PostsPlugin, *Handle, *models.Posts)) []func(PostsPlugin, *Handle, *models.Posts) {
return slice.FilterAndMap(name, func(t string) (func(PostsPlugin, *Handle, *models.Posts), bool) {
v, ok := m[t]
if ok {
return v, true
}
return nil, false
})
}

View File

@ -1,167 +0,0 @@
package middleware
import (
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/app/theme/wp/components/widget"
"github.com/fthvgb1/wp-go/app/theme/wp/route"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/number"
str "github.com/fthvgb1/wp-go/helper/strings"
"net/http"
"strconv"
"time"
)
var plainRouteParam = reload.Vars([]Plain{
{
Action: "p",
Param: map[string]string{
"p": "id",
"cpage": "page",
},
Scene: constraints.Detail,
},
{
Action: "s",
Scene: constraints.Search,
},
{
Scene: constraints.Category,
Fn: func(h *wp.Handle) bool {
c, ok := widget.IsCategory(h)
if !ok {
return false
}
h.C.AddParam("category", c.Name)
h.C.AddParam("page", h.C.Query("paged"))
return true
},
},
{
Scene: constraints.Tag,
Action: "tag",
Param: map[string]string{
"tag": "tag",
"paged": "page",
},
},
{
Scene: constraints.Archive,
Fn: func(h *wp.Handle) bool {
m := h.C.Query("m")
if m == "" {
return false
}
t, err := time.Parse("200601", m)
if err != nil {
return false
}
h.C.AddParam("year", strconv.Itoa(t.Year()))
h.C.AddParam("month", number.IntToString(t.Month()))
h.C.AddParam("page", h.C.Query("paged"))
return true
},
},
{
Scene: constraints.Author,
Fn: func(h *wp.Handle) bool {
u := h.C.Query("author")
if u == "" {
return false
}
users := GetUsersIds(h)
name, ok := users[str.ToInteger[uint64](u, 0)]
if !ok {
return false
}
h.C.AddParam("author", name)
h.C.AddParam("page", h.C.Query("paged"))
return true
},
},
}, "plainRouteParam")
var GetUsersIds = reload.BuildValFnWithConfirm("usersIds", func(h *wp.Handle) (map[uint64]string, bool) {
users, err := cache.GetAllUsername(h.C)
if err != nil {
return nil, false
}
return maps.Flip(users), true
}, 10)
func SetExplainRouteParam(p []Plain) {
plainRouteParam.Store(p)
}
func GetExplainRouteParam() []Plain {
return plainRouteParam.Load()
}
func PushExplainRouteParam(explain ...Plain) {
v := plainRouteParam.Load()
v = append(v, explain...)
plainRouteParam.Store(v)
}
type Plain struct {
Action string
Param map[string]string
Scene string
Fn func(h *wp.Handle) bool
}
func MixWithPlain(h *wp.Handle) {
for _, explain := range plainRouteParam.Load() {
if explain.Action == "" && explain.Fn == nil {
continue
}
if explain.Fn != nil {
if !explain.Fn(h) {
continue
}
if explain.Scene != "" {
h.SetScene(explain.Scene)
}
wp.Run(h, nil)
h.Abort()
return
}
if explain.Scene == "" {
continue
}
q := h.C.Query(explain.Action)
if q == "" {
continue
}
h.SetScene(explain.Scene)
for query, param := range explain.Param {
h.C.AddParam(param, h.C.Query(query))
}
wp.Run(h, nil)
h.Abort()
return
}
}
func ShowPreComment(h *wp.Handle) {
v, ok := cache.NewCommentCache().Get(h.C, h.C.Request.URL.RawQuery)
if ok {
h.C.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
h.C.Writer.WriteHeader(http.StatusOK)
_, _ = h.C.Writer.Write([]byte(v))
h.Abort()
}
}
func CommonMiddleware(h *wp.Handle) {
h.PushHandler(constraints.PipeMiddleware, constraints.Home,
wp.NewHandleFn(MixWithPlain, 100, "middleware.MixWithPlain"),
)
h.PushHandler(constraints.PipeMiddleware, constraints.Detail,
wp.NewHandleFn(ShowPreComment, 100, "middleware.ShowPreComment"),
)
h.PushHandler(constraints.PipeMiddleware, constraints.NoRoute,
wp.NewHandleFn(route.ResolveRoute, 100, "route.ResolveRoute"),
)
}

View File

@ -1,247 +0,0 @@
package wp
import (
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
)
type HandlePipeFn[T any] func(HandleFn[T], T)
type Pipe struct {
Name string
Order float64
Fn HandlePipeFn[*Handle]
}
func NewPipe(name string, order float64, fn HandlePipeFn[*Handle]) Pipe {
return Pipe{Name: name, Order: order, Fn: fn}
}
// HandlePipe 方便把功能写在其它包里
func HandlePipe[T any](initial func(T), fns ...HandlePipeFn[T]) HandleFn[T] {
return slice.ReverseReduce(fns, func(next HandlePipeFn[T], f HandleFn[T]) HandleFn[T] {
return func(t T) {
next(f, t)
}
}, initial)
}
func (h *Handle) PushPipe(scene string, pipes ...Pipe) error {
return PushFn("pipe", scene, pipes...)
}
func (h *Handle) PushPipeHook(scene string, pipes ...func(Pipe) (Pipe, bool)) error {
return PushFnHook("pipeHook", scene, pipes...)
}
func (h *Handle) DeletePipe(scene, pipeName string) error {
return PushFnHook("pipeHook", scene, func(pipe Pipe) (Pipe, bool) {
return pipe, pipeName != pipe.Name
})
}
func (h *Handle) ReplacePipe(scene, pipeName string, pipe Pipe) error {
return PushFnHook("pipeHook", scene, func(p Pipe) (Pipe, bool) {
if pipeName == p.Name {
p = pipe
}
return p, true
})
}
func (h *Handle) PushHandler(pipScene string, scene string, fns ...HandleCall) {
v, ok := handlerss.Load(pipScene)
if !ok {
v = make(map[string][]HandleCall)
}
v[scene] = append(v[scene], fns...)
handlerss.Store(pipScene, v)
}
func (h *Handle) PushRender(statsOrScene string, fns ...HandleCall) {
h.PushHandler(constraints.PipeRender, statsOrScene, fns...)
}
func (h *Handle) PushDataHandler(scene string, fns ...HandleCall) {
h.PushHandler(constraints.PipeData, scene, fns...)
}
func BuildHandlers(pipeScene string, keyFn func(*Handle, string) string,
handleHookFn func(*Handle, string,
map[string][]func(HandleCall) (HandleCall, bool),
map[string][]HandleCall) []HandleCall) func(HandleFn[*Handle], *Handle) {
pipeHandlerFn := reload.BuildMapFn[string]("pipeHandlers", BuildHandler(pipeScene, keyFn, handleHookFn))
return func(next HandleFn[*Handle], h *Handle) {
key := keyFn(h, pipeScene)
handlers := pipeHandlerFn(key, h)
for _, handler := range handlers {
handler.Fn(h)
if h.abort {
break
}
}
if !h.stopPipe {
next(h)
}
}
}
func BuildHandler(pipeScene string, keyFn func(*Handle, string) string,
handleHook func(*Handle, string,
map[string][]func(HandleCall) (HandleCall, bool),
map[string][]HandleCall) []HandleCall) func(*Handle) []HandleCall {
return func(h *Handle) []HandleCall {
key := keyFn(h, pipeScene)
mut := reload.GetGlobeMutex()
mut.Lock()
pipeHookers, _ := handleHooks.Load(pipeScene)
pipeHandlers, _ := handlerss.Load(pipeScene)
mut.Unlock()
calls := handleHook(h, key, pipeHookers, pipeHandlers)
slice.SimpleSort(calls, slice.DESC, func(t HandleCall) float64 {
return t.Order
})
return calls
}
}
func HookHandles(hooks map[string][]func(HandleCall) (HandleCall, bool),
handlers map[string][]HandleCall, sceneOrStats ...string) []HandleCall {
hookedHandlers := slice.FilterAndMap(sceneOrStats, func(k string) ([]HandleCall, bool) {
r := handlers[k]
for _, hook := range hooks[k] {
r = slice.FilterAndMap(r, hook)
}
return r, true
})
return slice.Decompress(hookedHandlers)
}
func PipeKey(h *Handle, pipScene string) string {
key := str.Join("pipekey", "-", pipScene, "-", h.scene, "-", h.Stats)
return h.DoActionFilter("pipeKey", key, pipScene)
}
var pipeInitFn = reload.BuildMapFn[string]("pipeInit", BuildPipe)
func Run(h *Handle, conf func(*Handle)) {
if !h.isInited {
InitHandle(conf, h)
}
pipeInitFn(h.scene, h.scene)(h)
}
func BuildPipe(scene string) func(*Handle) {
pipees := GetFn[Pipe]("pipe", constraints.AllScene)
pipees = append(pipees, GetFn[Pipe]("pipe", scene)...)
pipes := slice.FilterAndMap(pipees, func(pipe Pipe) (Pipe, bool) {
var ok bool
mut := reload.GetGlobeMutex()
mut.Lock()
hooks := GetFnHook[func(Pipe) (Pipe, bool)]("pipeHook", constraints.AllScene)
hooks = append(hooks, GetFnHook[func(Pipe) (Pipe, bool)]("pipeHook", scene)...)
mut.Unlock()
for _, fn := range hooks {
pipe, ok = fn(pipe)
if !ok {
return pipe, false
}
}
return pipe, pipe.Fn != nil
})
slice.SimpleSort(pipes, slice.DESC, func(t Pipe) float64 {
return t.Order
})
arr := slice.Map(pipes, func(t Pipe) HandlePipeFn[*Handle] {
return t.Fn
})
return HandlePipe(NothingToDo, arr...)
}
func MiddlewareKey(h *Handle, pipScene string) string {
return h.DoActionFilter("middleware", str.Join("pipe-middleware-", h.scene), pipScene)
}
func PipeMiddlewareHandle(h *Handle, key string,
hooks map[string][]func(HandleCall) (HandleCall, bool),
handlers map[string][]HandleCall) []HandleCall {
hookedHandles := HookHandles(hooks, handlers, h.scene, constraints.AllScene)
return h.PipeHandleHook("PipeMiddlewareHandle", hookedHandles, handlers, key)
}
func PipeDataHandle(h *Handle, key string,
hooks map[string][]func(HandleCall) (HandleCall, bool),
handlers map[string][]HandleCall) []HandleCall {
hookedHandles := HookHandles(hooks, handlers, h.scene, constraints.AllScene)
return h.PipeHandleHook("PipeDataHandle", hookedHandles, handlers, key)
}
func PipeRender(h *Handle, key string,
hooks map[string][]func(HandleCall) (HandleCall, bool),
handlers map[string][]HandleCall) []HandleCall {
hookedHandles := HookHandles(hooks, handlers, h.scene, h.Stats, constraints.AllScene, constraints.AllStats)
return h.PipeHandleHook("PipeRender", hookedHandles, handlers, key)
}
// DeleteHandle 写插件的时候用
func (h *Handle) DeleteHandle(pipeScene, scene, name string) {
v, ok := handleHooks.Load(pipeScene)
if !ok {
v = make(map[string][]func(HandleCall) (HandleCall, bool))
}
v[scene] = append(v[scene], func(call HandleCall) (HandleCall, bool) {
return call, name != call.Name
})
handleHooks.Store(pipeScene, v)
}
// ReplaceHandle 写插件的时候用
func (h *Handle) ReplaceHandle(pipeScene, scene, name string, fn HandleFn[*Handle]) {
v, ok := handleHooks.Load(pipeScene)
if !ok {
v = make(map[string][]func(HandleCall) (HandleCall, bool))
}
v[scene] = append(v[scene], func(call HandleCall) (HandleCall, bool) {
if name == call.Name {
call.Fn = fn
}
return call, true
})
handleHooks.Store(pipeScene, v)
}
// HookHandle 写插件的时候用
func (h *Handle) HookHandle(pipeScene, scene string, hook func(HandleCall) (HandleCall, bool)) {
v, ok := handleHooks.Load(pipeScene)
if !ok {
v = make(map[string][]func(HandleCall) (HandleCall, bool))
}
v[scene] = append(v[scene], hook)
handleHooks.Store(pipeScene, v)
}
func (h *Handle) PushPipeHandleHook(name string, fn ...func([]HandleCall) []HandleCall) error {
return PushFnHook("pipeHandleHook", name, fn...)
}
func (h *Handle) PipeHandleHook(name string, calls []HandleCall, m map[string][]HandleCall, key string) []HandleCall {
fn := GetFnHook[func(*Handle, []HandleCall, map[string][]HandleCall, string) []HandleCall]("pipeHandleHook", name)
return slice.Reduce(fn, func(t func(*Handle, []HandleCall, map[string][]HandleCall, string) []HandleCall, r []HandleCall) []HandleCall {
return t(h, r, m, key)
}, calls)
}
func InitPipe(h *Handle) {
h.PushPipe(constraints.AllScene, NewPipe(constraints.PipeMiddleware, 300,
BuildHandlers(constraints.PipeMiddleware, MiddlewareKey, PipeMiddlewareHandle)))
h.PushPipe(constraints.AllScene, NewPipe(constraints.PipeData, 200,
BuildHandlers(constraints.PipeData, PipeKey, PipeDataHandle)))
h.PushPipe(constraints.AllScene, NewPipe(constraints.PipeRender, 100,
BuildHandlers(constraints.PipeRender, PipeKey, PipeRender)))
}

View File

@ -1,146 +0,0 @@
package route
import (
"github.com/fthvgb1/wp-go/app/theme/wp"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/slice"
"github.com/fthvgb1/wp-go/safety"
"net/http"
"regexp"
)
// Route
// Type value equal const or reg
type Route struct {
Path string
Scene string
Method []string
Type string
}
var routeHook []func(Route) (Route, bool)
var regRoutes *safety.Map[string, *regexp.Regexp]
var routes = func() *safety.Map[string, Route] {
r := safety.NewMap[string, Route]()
reload.Append(func() {
r.Flush()
regRoutes.Flush()
}, "wp-routers")
regRoutes = safety.NewMap[string, *regexp.Regexp]()
return r
}()
// PushRoute path can be const or regex string
//
// eg: `(?P<controller>\w+)/(?P<method>\w+)`, route.Route{
// Path: `(?P<controller>\w+)/(?P<method>\w+)`,
// Scene: constraints.Home,
// Method: []string{"GET"},
// Type: "reg",
// }
func PushRoute(path string, route Route) error {
if route.Type == "const" {
routes.Store(path, route)
return nil
}
re, err := regexp.Compile(route.Path)
if err != nil {
return err
}
regRoutes.Store(path, re)
routes.Store(path, route)
return err
}
func Delete(path string) {
routeHook = append(routeHook, func(route Route) (Route, bool) {
return route, route.Path != path
})
}
func Replace(path string, route Route) {
routeHook = append(routeHook, func(r Route) (Route, bool) {
return route, path == route.Path
})
}
func Hook(path string, fn func(Route) Route) {
routeHook = append(routeHook, func(r Route) (Route, bool) {
if path == r.Path {
r = fn(r)
}
return r, path == r.Path
})
}
var RegRouteFn = reload.BuildValFnWithAnyParams("regexRoute", RegRouteHook)
func RegRouteHook(_ ...any) func() (map[string]Route, map[string]*regexp.Regexp) {
m := map[string]Route{}
rrs := map[string]*regexp.Regexp{}
routes.Range(func(key string, value Route) bool {
vv, _ := regRoutes.Load(key)
if len(routeHook) > 0 {
for _, fn := range routeHook {
v, ok := fn(value)
if !ok {
continue
}
m[v.Path] = v
if v.Type != "reg" {
continue
}
if v.Path != key {
vvv, err := regexp.Compile(v.Path)
if err != nil {
panic(err)
}
vv = vvv
}
rrs[v.Path] = vv
}
} else {
m[key] = value
rrs[key] = vv
}
return true
})
return func() (map[string]Route, map[string]*regexp.Regexp) {
return m, rrs
}
}
func ResolveRoute(h *wp.Handle) {
requestURI := h.C.Request.RequestURI
rs, rrs := RegRouteFn()()
v, ok := rs[requestURI]
if ok && slice.IsContained(v.Method, h.C.Request.Method) {
h.SetScene(v.Scene)
wp.Run(h, nil)
h.Abort()
return
}
for path, reg := range rrs {
r := reg.FindStringSubmatch(requestURI)
if len(r) < 1 {
return
}
rr := rs[path]
if slice.IsContained(rr.Method, h.C.Request.Method) {
h.SetScene(rr.Scene)
for i, name := range reg.SubexpNames() {
if name == "" {
continue
}
h.C.AddParam(name, r[i])
}
h.C.Set("regRoute", reg)
h.C.Set("regRouteRes", r)
wp.Run(h, nil)
h.Abort()
return
}
}
h.C.Status(http.StatusNotFound)
h.Abort()
}

View File

@ -1,48 +0,0 @@
package wp
import (
"fmt"
"github.com/elliotchance/phpserialize"
"github.com/fthvgb1/wp-go/app/pkg/cache"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
)
var GetStickPosts = reload.BuildValFnWithConfirm("stickPostsSlice", ParseStickPosts)
func ParseStickPosts(h *Handle) (r []models.Posts, ok bool) {
v := wpconfig.GetOption("sticky_posts")
if v == "" {
return
}
array, err := phpserialize.UnmarshalIndexedArray([]byte(v))
if err != nil {
logs.Error(err, "解析option sticky_posts错误", v)
return
}
r = slice.FilterAndMap(array, func(t any) (models.Posts, bool) {
id := str.ToInt[uint64](fmt.Sprintf("%v", t))
post, err := cache.GetPostById(h.C, id)
post.IsSticky = true
return post, err == nil
})
ok = true
return
}
var GetStickMapPosts = reload.BuildValFn("stickPostsMap", StickMapPosts)
func StickMapPosts(h *Handle) map[uint64]models.Posts {
return slice.SimpleToMap(GetStickPosts(h), func(v models.Posts) uint64 {
return v.Id
})
}
func (h *Handle) IsStick(id uint64) bool {
_, ok := GetStickMapPosts(h)[id]
return ok
}

View File

@ -1,32 +0,0 @@
{{define "common/head"}}
{{ callFuncString .calComponent "headScript"}}
{{if .externHead}}
{{.externHead|unescaped}}
{{end}}
{{end}}
{{define "common/footer"}}
{{ callFuncString .calComponent "footerScript"}}
{{if .externFooter}}
{{.externFooter|unescaped}}
{{end}}
{{end}}
{{define "common/sidebarWidget"}}
{{ callFuncString .calComponent "sidebarsWidgets"}}
{{end}}
{{define "common/colophon"}}
{{if .colophon}}
{{.colophon|unescaped}}
{{else}}
<footer id="colophon" class="site-footer">
<div class="site-info">
<a href="https://github.com/fthvgb1/wp-go" class="imprint">自豪地采用 wp-go</a>
</div>
</footer>
{{end}}
{{end}}

View File

@ -1,305 +0,0 @@
package wp
import (
"errors"
"github.com/fthvgb1/wp-go/app/pkg/config"
"github.com/fthvgb1/wp-go/app/pkg/constraints"
"github.com/fthvgb1/wp-go/app/pkg/logs"
"github.com/fthvgb1/wp-go/app/plugins/wphandle/apply"
"github.com/fthvgb1/wp-go/app/wpconfig"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper/maps"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"html/template"
"net/http"
"os"
"strings"
)
type Handle struct {
C *gin.Context
theme string
isInited bool
Session sessions.Session
ginH gin.H
password string
scene string
Code int
Stats string
templ string
themeMods wpconfig.ThemeMods
err error
errLevel int8
abort bool
stopPipe bool
}
var handlerss = safety.NewMap[string, map[string][]HandleCall]()
var handleHooks = safety.NewMap[string, map[string][]func(HandleCall) (HandleCall, bool)]()
func (h *Handle) Theme() string {
return h.theme
}
func (h *Handle) GinH() gin.H {
return h.ginH
}
func (h *Handle) SetScene(scene string) {
h.scene = scene
}
func (h *Handle) Components() *safety.Map[string, map[string][]Components[string]] {
return handleComponents
}
func (h *Handle) ComponentHook() *safety.Map[string, map[string][]func(Components[string]) (Components[string], bool)] {
return handleComponentHook
}
func (h *Handle) Handlers() *safety.Map[string, map[string][]HandleCall] {
return handlerss
}
func (h *Handle) HandleHook() *safety.Map[string, map[string][]func(HandleCall) (HandleCall, bool)] {
return handleHooks
}
type HandlePlugins map[string]HandleFn[*Handle]
// Components Order 为执行顺序,降序执行
type Components[T any] struct {
Name string
Val T
Fn func(*Handle) T
Order float64
Cached bool
}
type HandleFn[T any] func(T)
type HandleCall struct {
Fn HandleFn[*Handle]
Order float64
Name string
}
var isFirstRequest = true
func SetConfigHandle(a ...any) Handle {
configFn := a[0].(func(*Handle))
hh := a[1].(*Handle)
h := &Handle{}
handleComponents.Flush()
componentsArgs.Flush()
handleComponentHook.Flush()
componentFilterFns.Flush()
handlerss.Flush()
handleHooks.Flush()
h.ginH = gin.H{}
fnMap.Flush()
fnHook.Flush()
if isFirstRequest {
isFirstRequest = false
} else {
reload.Reload()
}
h.C = hh.C
h.theme = hh.theme
configFn(h)
v := apply.GetPlugins()
pluginFn, ok := v.(func(*Handle))
if ok {
pluginFn(h)
}
return *h
}
var GetInitHandleFn = reload.BuildValFnWithAnyParams("themeArgAndConfig", SetConfigHandle, false)
func InitHandle(configFn func(*Handle), h *Handle) {
hh := GetInitHandleFn(configFn, h)
mods, err := wpconfig.GetThemeMods(h.theme)
logs.IfError(err, "获取mods失败")
h.themeMods = mods
h.ginH = maps.Copy(hh.ginH)
h.ginH["calPostClass"] = postClass(h)
h.ginH["calBodyClass"] = bodyClass(h)
h.ginH["customLogo"] = customLogo(h)
h.ginH["calComponent"] = CalComponent(h)
h.isInited = true
}
func (h *Handle) Abort() {
h.abort = true
h.stopPipe = true
}
func (h *Handle) StopPipe() {
h.stopPipe = true
}
func (h *Handle) StopHandle() {
h.abort = true
}
func (h *Handle) CommonThemeMods() wpconfig.ThemeMods {
return h.themeMods
}
func (h *Handle) Err() error {
return h.err
}
func (h *Handle) SetErr(err error, level int8) {
h.err = errors.Join(err)
h.errLevel = level
}
func (h *Handle) ErrLevel() int8 {
return h.errLevel
}
func (h *Handle) SetErrLevel(errLevel int8) {
h.errLevel = errLevel
}
func (h *Handle) SetTempl(templ string) {
h.templ = templ
}
func (h *Handle) GetTempl() string {
return h.templ
}
func (h *Handle) Scene() string {
return h.scene
}
func (h *Handle) SetDatas(GinH gin.H) {
maps.Merge(h.ginH, GinH)
}
func (h *Handle) SetData(k string, v any) {
h.ginH[k] = v
}
func NewHandle(c *gin.Context, scene string, theme string) *Handle {
return &Handle{
C: c,
theme: theme,
Session: sessions.Default(c),
scene: scene,
Stats: constraints.Ok,
}
}
func (h *Handle) GetPassword() string {
if h.password != "" {
return h.password
}
pw := h.Session.Get("post_password")
if pw != nil {
h.password = pw.(string)
}
return h.password
}
func PreTemplate(h *Handle) {
if h.templ == "" {
h.templ = str.Join(h.theme, "/posts/index.gohtml")
if h.scene == constraints.Detail {
h.templ = str.Join(h.theme, "/posts/detail.gohtml")
}
}
}
func PreCodeAndStats(h *Handle) {
if h.Stats != "" && h.Code != 0 {
return
}
switch h.Stats {
case constraints.Ok:
h.Code = http.StatusOK
case constraints.ParamError, constraints.Error404:
h.Code = http.StatusNotFound
case constraints.InternalErr:
h.Code = http.StatusInternalServerError
}
}
var htmlContentType = []string{"text/html; charset=utf-8"}
func (h *Handle) RenderHtml(t *template.Template, statsCode int, name string) {
header := h.C.Writer.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = htmlContentType
}
h.C.Status(statsCode)
var err error
if name == "" {
err = t.Execute(h.C.Writer, h.ginH)
} else {
err = t.ExecuteTemplate(h.C.Writer, name, h.ginH)
}
h.Abort()
if err != nil {
panic(err)
}
}
func (h *Handle) PushHandlers(pipeScene string, call HandleCall, statsOrScene ...string) {
for _, s := range statsOrScene {
h.PushHandler(pipeScene, s, call)
}
}
func (h *Handle) CommonComponents() {
h.PushCacheGroupHeadScript(constraints.AllScene, "siteIconAndCustomCss", 0, CalSiteIcon, CalCustomCss)
h.PushRender(constraints.AllStats, NewHandleFn(PreRenderTemplate, 0, "wp.PreRenderTemplate"))
ReplyCommentJs(h)
AdditionScript(h)
}
func AdditionScript(h *Handle) {
s := config.GetConfig().ExternScript
if len(s) < 1 {
return
}
fn := func(f, name string) {
if f == "" {
return
}
ss, err := os.ReadFile(f)
if err != nil {
logs.Error(err, str.Join("解析", name, "失败"), f)
} else {
h.PushComponents(constraints.AllScene, constraints.HeadScript, NewComponent(name, string(ss), false, 0, nil))
}
}
switch len(s) {
case 1:
fn(s[0], "externHead")
case 2:
fn(s[0], "externHead")
fn(s[1], "externFooter")
}
}
func PreRenderTemplate(h *Handle) {
h.C.HTML(h.Code, h.templ, h.ginH)
h.Abort()
}
func NewHandleFn(fn HandleFn[*Handle], order float64, name string) HandleCall {
return HandleCall{Fn: fn, Order: order, Name: name}
}
func NothingToDo(h *Handle) {
h.Abort()
}
func (h *Handle) IsHttps() bool {
if h.C.Request.TLS != nil {
return true
}
return "https" == strings.ToLower(h.C.Request.Header.Get("X-Forwarded-Proto"))
}

View File

@ -1,49 +0,0 @@
package wpconfig
import (
"github.com/fthvgb1/wp-go/app/pkg/models"
"github.com/fthvgb1/wp-go/model"
"github.com/fthvgb1/wp-go/safety"
)
var terms = safety.NewMap[uint64, models.Terms]()
var termTaxonomies = safety.NewMap[uint64, models.TermTaxonomy]()
var my = safety.NewMap[uint64, models.TermsMy]()
func GetTerm(termId uint64) (models.Terms, bool) {
return terms.Load(termId)
}
func GetTermTaxonomy(termId uint64) (models.TermTaxonomy, bool) {
return termTaxonomies.Load(termId)
}
func GetTermMy(termId uint64) (models.TermsMy, bool) {
return my.Load(termId)
}
func InitTerms() (err error) {
terms.Flush()
termTaxonomies.Flush()
term, err := model.SimpleFind[models.Terms](ctx, nil, "*")
if err != nil {
return err
}
for _, wpTerms := range term {
terms.Store(wpTerms.TermId, wpTerms)
}
termTax, err := model.SimpleFind[models.TermTaxonomy](ctx, nil, "*")
if err != nil {
return err
}
for _, taxonomy := range termTax {
termTaxonomies.Store(taxonomy.TermTaxonomyId, taxonomy)
if term, ok := terms.Load(taxonomy.TermId); ok {
my.Store(taxonomy.TermId, models.TermsMy{
Terms: term,
TermTaxonomy: taxonomy,
})
}
}
return
}

44
cache/cache.go vendored
View File

@ -2,49 +2,15 @@ package cache
import (
"context"
"sync"
"time"
)
type Cache[K comparable, V any] interface {
Get(ctx context.Context, key K) (V, bool)
Set(ctx context.Context, key K, val V)
GetExpireTime(ctx context.Context) time.Duration
Ttl(ctx context.Context, key K) time.Duration
Set(ctx context.Context, key K, val V, expire time.Duration)
Ttl(ctx context.Context, key K, expire time.Duration) time.Duration
Ver(ctx context.Context, key K) int
Flush(ctx context.Context)
Del(ctx context.Context, key ...K)
ClearExpired(ctx context.Context)
}
type Expend[K comparable, V any] interface {
Gets(ctx context.Context, k []K) (map[K]V, error)
Sets(ctx context.Context, m map[K]V)
}
type SetTime interface {
SetExpiredTime(func() time.Duration)
}
type AnyCache[T any] interface {
Get(ctx context.Context) (T, bool)
Set(ctx context.Context, v T)
Flush(ctx context.Context)
GetLastSetTime(ctx context.Context) time.Time
}
type Refresh[K comparable, V any] interface {
Refresh(ctx context.Context, k K, a ...any)
}
type RefreshVar[T any] interface {
Refresh(ctx context.Context, a ...any)
}
type Lockss[K comparable] interface {
GetLock(ctx context.Context, gMut *sync.Mutex, k ...K) *sync.Mutex
}
type LockFn[K comparable] func(ctx context.Context, gMut *sync.Mutex, k ...K) *sync.Mutex
type LocksNum interface {
SetLockNum(num int)
Delete(ctx context.Context, key K)
ClearExpired(ctx context.Context, expire time.Duration)
}

View File

@ -1,163 +0,0 @@
package cachemanager
import (
"context"
"github.com/fthvgb1/wp-go/cache"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/slice/mockmap"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"runtime"
"sync"
"time"
)
var mutex sync.Mutex
type Fn func(context.Context)
type clearExpired interface {
ClearExpired(ctx context.Context)
}
var clears = safety.NewVar(mockmap.Map[string, Fn]{})
var flushes = safety.NewVar(mockmap.Map[string, Fn]{})
func Flush() {
ctx := context.WithValue(context.Background(), "execFlushBy", "mangerFlushFn")
for _, f := range flushes.Load() {
f.Value(ctx)
}
}
func Flushes(ctx context.Context, names ...string) {
execute(ctx, flushes, names...)
}
func execute(ctx context.Context, q *safety.Var[mockmap.Map[string, Fn]], names ...string) {
queues := q.Load()
for _, name := range names {
queue := queues.Get(name)
if queue.Value != nil {
queue.Value(ctx)
}
}
}
func parseArgs(args ...any) (string, func() time.Duration) {
var name string
var fn func() time.Duration
for _, arg := range args {
v, ok := arg.(string)
if ok {
name = v
continue
}
vv, ok := arg.(func() time.Duration)
if ok {
fn = vv
}
}
return name, fn
}
func buildLockFn[K comparable](args ...any) cache.LockFn[K] {
lockFn := helper.ParseArgs(cache.LockFn[K](nil), args...)
name := helper.ParseArgs("", args...)
num := helper.ParseArgs(runtime.NumCPU(), args...)
loFn := func() int {
return num
}
loFn = helper.ParseArgs(loFn, args...)
if name != "" {
loFn = reload.BuildFnVal(str.Join("cachesLocksNum-", name), num, loFn)
}
if lockFn == nil {
looo := helper.ParseArgs(cache.Lockss[K](nil), args...)
if looo != nil {
lockFn = looo.GetLock
loo, ok := any(looo).(cache.LocksNum)
if ok && loo != nil {
loo.SetLockNum(num)
}
} else {
lo := cache.NewLocks[K](loFn)
lockFn = lo.GetLock
PushOrSetFlush(mockmap.Item[string, Fn]{
Name: name,
Value: lo.Flush,
})
}
}
return lockFn
}
func SetExpireTime(c cache.SetTime, name string, expireTime time.Duration, expireTimeFn func() time.Duration) {
if name == "" {
return
}
fn := reload.BuildFnVal(str.Join("cacheManger-", name, "-expiredTime"), expireTime, expireTimeFn)
c.SetExpiredTime(fn)
}
func ChangeExpireTime(t time.Duration, coverConf bool, name ...string) {
for _, s := range name {
reload.SetFnVal(s, t, coverConf)
}
}
func pushOrSet(q *safety.Var[mockmap.Map[string, Fn]], queues ...mockmap.Item[string, Fn]) {
mutex.Lock()
defer mutex.Unlock()
qu := q.Load()
for _, queue := range queues {
v := qu.Get(queue.Name)
if v.Value != nil {
qu.Set(queue.Name, queue.Value)
} else {
qu = append(qu, queue)
}
}
q.Store(qu)
}
// PushOrSetFlush will execute flush func when call Flush or Flushes
func PushOrSetFlush(queues ...mockmap.Item[string, Fn]) {
pushOrSet(flushes, queues...)
}
// PushOrSetClearExpired will execute clearExpired func when call ClearExpired or ClearExpireds
func PushOrSetClearExpired(queues ...mockmap.Item[string, Fn]) {
pushOrSet(clears, queues...)
}
func del(q *safety.Var[mockmap.Map[string, Fn]], names ...string) {
mutex.Lock()
defer mutex.Unlock()
queues := q.Load()
for _, name := range names {
queues.Del(name)
}
q.Store(queues)
}
func DelFlush(names ...string) {
del(flushes, names...)
}
func DelClearExpired(names ...string) {
del(clears, names...)
}
func ClearExpireds(ctx context.Context, names ...string) {
execute(ctx, clears, names...)
}
func ClearExpired() {
ctx := context.WithValue(context.Background(), "execClearExpired", "mangerClearExpiredFn")
for _, queue := range clears.Load() {
queue.Value(ctx)
}
}

View File

@ -1,193 +0,0 @@
package cachemanager
import (
"context"
"fmt"
"github.com/fthvgb1/wp-go/helper/number"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/taskPools"
"reflect"
"strings"
"testing"
"time"
)
var ctx = context.Background()
func TestFlushMapVal(t *testing.T) {
_ = number.Range(1, 5, 0)
t.Run("t1", func(t *testing.T) {
count := 0
vv := NewMemoryMapCache(func(ctx2 context.Context, ks []int, a ...any) (map[int]int, error) {
r := make(map[int]int)
for _, k := range ks {
r[k] = k * k
}
count++
return r, nil
}, nil, time.Second, "test")
gets, err := GetBatchBy[int]("test", ctx, number.Range(1, 10), time.Second)
if err != nil {
t.Fatal(t, "err:", err)
}
p := taskPools.NewPools(10)
for i := 0; i < 20; i++ {
i := i
p.Execute(func() {
if i%2 == 0 {
vv.Get(ctx, 5)
} else {
vv.Set(ctx, i, i)
}
})
}
p.Wait()
fmt.Println(gets, count)
DelMapCacheVal("test", 3, 4)
fmt.Println(vv.Get(ctx, 3))
fmt.Println(vv.Get(ctx, 4))
get, err := GetBy[int]("test", ctx, 3, time.Second)
if err != nil {
t.Fatal(t, "err", err)
}
fmt.Println(get, count)
fmt.Println(vv.Get(ctx, 5))
Flushes(ctx, "test")
fmt.Println(vv.Get(ctx, 5))
fmt.Println(vv.Get(ctx, 6))
//fmt.Println(GetVarCache("test"))
})
}
func TestSetExpireTime(t *testing.T) {
t.Run("t1", func(t *testing.T) {
c := NewMemoryMapCache[string, string](func(ctx2 context.Context, strings []string, a ...any) (map[string]string, error) {
return slice.ToMap(strings, func(v string) (string, string) {
return v, str.Join(v, "__", v)
}, false), nil
}, nil, time.Second, "xx")
c.Set(ctx, "xx", "yy")
fmt.Println(c.Get(ctx, "xx"))
time.Sleep(time.Second)
fmt.Println(c.Get(ctx, "xx"))
ChangeExpireTime(3*time.Second, true, "xx")
c.Set(ctx, "xx", "yyy")
time.Sleep(time.Second)
fmt.Println(c.Get(ctx, "xx"))
time.Sleep(3 * time.Second)
fmt.Println(c.Get(ctx, "xx"))
cc, _ := GetMapCache[string, string]("xx")
fmt.Println(reflect.DeepEqual(c, cc))
cc.Set(ctx, "fff", "xxxx")
cc.Set(ctx, "ffx", "eex")
cc.Set(ctx, "ww", "vv")
m, err := cc.GetBatchToMap(ctx, []string{"fff", "ffx", "ww", "kkkk"}, time.Second)
fmt.Println(m, err)
fmt.Println(GetBatchByToMap[string]("xx", ctx, []string{"fff", "ffx", "ww", "kkkk"}, time.Second))
v := NewVarMemoryCache(func(ct context.Context, a ...any) (string, error) {
return "ssss", nil
}, 3*time.Second, "ff")
vv, _ := GetVarCache[string]("ff")
fmt.Println(reflect.DeepEqual(v, vv))
})
}
func TestSetMapCache(t *testing.T) {
t.Run("t1", func(t *testing.T) {
x := NewMemoryMapCache(nil, func(ctx2 context.Context, k string, a ...any) (string, error) {
fmt.Println("memory cache")
return strings.Repeat(k, 2), nil
}, time.Hour, "test")
fmt.Println(GetBy[string]("test", ctx, "test", time.Second))
NewMapCache[string, string](xx[string, string]{m: map[string]string{}}, nil, func(ctx2 context.Context, k string, a ...any) (string, error) {
fmt.Println("other cache drives. eg: redis,file.....")
return strings.Repeat(k, 2), nil
}, "test", time.Hour)
if err := SetMapCache("kkk", x); err != nil {
t.Errorf("SetMapCache() error = %v, wantErr %v", err, nil)
}
fmt.Println(GetBy[string]("test", ctx, "test", time.Second))
})
}
type xx[K comparable, V any] struct {
m map[K]V
}
func (x xx[K, V]) Get(ctx context.Context, key K) (V, bool) {
v, ok := x.m[key]
return v, ok
}
func (x xx[K, V]) Set(ctx context.Context, key K, val V) {
x.m[key] = val
}
func (x xx[K, V]) GetExpireTime(ctx context.Context) time.Duration {
//TODO implement me
panic("implement me")
}
func (x xx[K, V]) Ttl(ctx context.Context, key K) time.Duration {
//TODO implement me
panic("implement me")
}
func (x xx[K, V]) Flush(ctx context.Context) {
//TODO implement me
panic("implement me")
}
func (x xx[K, V]) Del(ctx context.Context, key ...K) {
//TODO implement me
panic("implement me")
}
func (x xx[K, V]) ClearExpired(ctx context.Context) {
//TODO implement me
panic("implement me")
}
func TestSetVarCache(t *testing.T) {
t.Run("t1", func(t *testing.T) {
bak := NewVarMemoryCache(func(ctx2 context.Context, a ...any) (string, error) {
fmt.Println("memory cache")
return "xxx", nil
}, time.Hour, "test")
fmt.Println(GetVarVal[string]("test", ctx, time.Second))
NewVarCache[string](oo[string]{}, func(ctx2 context.Context, a ...any) (string, error) {
fmt.Println("other cache drives. eg: redis,file.....")
return "ooo", nil
}, "test")
if err := SetVarCache("xx", bak); err != nil {
t.Errorf("SetVarCache() error = %v, wantErr %v", err, nil)
}
fmt.Println(GetVarVal[string]("test", ctx, time.Second))
})
}
type oo[T any] struct {
val T
}
func (o oo[T]) Get(ctx context.Context) (T, bool) {
return o.val, false
}
func (o oo[T]) Set(ctx context.Context, v T) {
o.val = v
}
func (o oo[T]) Flush(ctx context.Context) {
//TODO implement me
panic("implement me")
}
func (o oo[T]) GetLastSetTime(ctx context.Context) time.Time {
//TODO implement me
panic("implement me")
}

View File

@ -1,148 +0,0 @@
package cachemanager
import (
"context"
"errors"
"github.com/fthvgb1/wp-go/cache"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/slice/mockmap"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"time"
)
var mapDelFuncs = safety.NewMap[string, func(any)]()
var mapCache = safety.NewMap[string, any]()
func SetMapCache[K comparable, V any](name string, ca *cache.MapCache[K, V]) error {
v, ok := mapCache.Load(name)
if !ok {
mapCache.Store(name, ca)
return nil
}
_, ok = v.(*cache.MapCache[K, V])
if !ok {
return errors.New(str.Join("cache ", name, " type err"))
}
mapCache.Store(name, ca)
return nil
}
// PushMangerMap will del mapCache val with name When call DelMapCacheVal
func PushMangerMap[K comparable, V any](name string, m *cache.MapCache[K, V]) {
if name == "" {
return
}
mapCache.Store(name, m)
mapDelFuncs.Store(name, func(a any) {
k, ok := a.([]K)
if ok && len(k) > 0 {
mm, ok := mapCache.Load(name)
if !ok {
return
}
c, ok := mm.(*cache.MapCache[K, V])
if !ok {
return
}
ctx := context.WithValue(context.Background(), "ctx", "registerFlush")
c.Del(ctx, k...)
}
})
}
func GetBy[T any, K comparable](name string, ct context.Context, key K, timeout time.Duration, params ...any) (r T, err error) {
ct = context.WithValue(ct, "getCache", name)
ca, err := getMap[K, T](name)
if err != nil {
return r, err
}
vv, err := ca.GetCache(ct, key, timeout, params...)
if err != nil {
return r, err
}
r = vv
return
}
func getMap[K comparable, T any](name string) (*cache.MapCache[K, T], error) {
m, ok := mapCache.Load(name)
if !ok {
return nil, errors.New(str.Join("cache ", name, " doesn't exist"))
}
vk, ok := m.(*cache.MapCache[K, T])
if !ok {
return nil, errors.New(str.Join("cache ", name, " type error"))
}
return vk, nil
}
func GetBatchBy[T any, K comparable](name string, ct context.Context, key []K, timeout time.Duration, params ...any) (r []T, err error) {
ct = context.WithValue(ct, "getCache", name)
ca, err := getMap[K, T](name)
if err != nil {
return r, err
}
vv, err := ca.GetCacheBatch(ct, key, timeout, params...)
if err != nil {
return r, err
}
r = vv
return
}
func GetBatchByToMap[T any, K comparable](name string, ct context.Context, key []K, timeout time.Duration, params ...any) (r map[K]T, err error) {
ct = context.WithValue(ct, "getCache", name)
ca, err := getMap[K, T](name)
if err != nil {
return r, err
}
vv, err := ca.GetBatchToMap(ct, key, timeout, params...)
if err != nil {
return r, err
}
r = vv
return
}
func NewMapCache[K comparable, V any](data cache.Cache[K, V], batchFn cache.MapBatchFn[K, V], fn cache.MapSingleFn[K, V], args ...any) *cache.MapCache[K, V] {
inc := helper.ParseArgs((*cache.IncreaseUpdate[K, V])(nil), args...)
m := cache.NewMapCache[K, V](data, fn, batchFn, inc, buildLockFn[K](args...), args...)
name, f := parseArgs(args...)
if name != "" {
PushMangerMap(name, m)
}
PushOrSetFlush(mockmap.Item[string, Fn]{
Name: name,
Value: m.Flush,
})
PushOrSetClearExpired(mockmap.Item[string, Fn]{
Name: name,
Value: m.ClearExpired,
})
if f != nil && name != "" {
SetExpireTime(any(data).(cache.SetTime), name, 0, f)
}
return m
}
func NewMemoryMapCache[K comparable, V any](batchFn cache.MapBatchFn[K, V],
fn cache.MapSingleFn[K, V], expireTime time.Duration, args ...any) *cache.MapCache[K, V] {
c := NewMapCache[K, V](cache.NewMemoryMapCache[K, V](func() time.Duration {
return expireTime
}), batchFn, fn, args...)
return c
}
func GetMapCache[K comparable, V any](name string) (*cache.MapCache[K, V], bool) {
vv, err := getMap[K, V](name)
return vv, err == nil
}
func DelMapCacheVal[T any](name string, keys ...T) {
v, ok := mapDelFuncs.Load(name)
if !ok || len(keys) < 1 {
return
}
v(keys)
}

View File

@ -1,57 +0,0 @@
package cachemanager
import (
"context"
"errors"
"github.com/fthvgb1/wp-go/cache"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper"
str "github.com/fthvgb1/wp-go/helper/strings"
"time"
)
func NewPaginationCache[K comparable, V any](m *cache.MapCache[string, helper.PaginationData[V]], maxNum int,
dbFn cache.DbFn[K, V], localFn cache.LocalFn[K, V], dbKeyFn, localKeyFn func(K, ...any) string, fetchNum int, name string, a ...any) *cache.Pagination[K, V] {
fn := helper.ParseArgs([]func() int(nil), a...)
var ma, fet func() int
if len(fn) > 0 {
ma = fn[0]
if len(fn) > 1 {
fet = fn[1]
}
}
if ma == nil {
ma = reload.BuildFnVal(str.Join("paginationCache-", name, "-maxNum"), maxNum, nil)
}
if fet == nil {
fet = reload.BuildFnVal(str.Join("paginationCache-", name, "-fetchNum"), fetchNum, nil)
}
p := cache.NewPagination(m, ma, dbFn, localFn, dbKeyFn, localKeyFn, fet, name)
mapCache.Store(name, p)
return p
}
func GetPaginationCache[K comparable, V any](name string) (*cache.Pagination[K, V], bool) {
v, err := getPagination[K, V](name)
return v, err == nil
}
func Pagination[V any, K comparable](name string, ctx context.Context, timeout time.Duration, k K, page, limit int, a ...any) ([]V, int, error) {
v, err := getPagination[K, V](name)
if err != nil {
return nil, 0, err
}
return v.Pagination(ctx, timeout, k, page, limit, a...)
}
func getPagination[K comparable, T any](name string) (*cache.Pagination[K, T], error) {
m, ok := mapCache.Load(name)
if !ok {
return nil, errors.New(str.Join("cache ", name, " doesn't exist"))
}
vk, ok := m.(*cache.Pagination[K, T])
if !ok {
return nil, errors.New(str.Join("cache ", name, " type error"))
}
return vk, nil
}

View File

@ -1,83 +0,0 @@
package cachemanager
import (
"context"
"errors"
"github.com/fthvgb1/wp-go/cache"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/slice/mockmap"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"time"
)
var varCache = safety.NewMap[string, any]()
func SetVarCache[T any](name string, v *cache.VarCache[T]) error {
vv, ok := varCache.Load(name)
if !ok {
varCache.Store(name, v)
return nil
}
_, ok = vv.(*cache.VarCache[T])
if ok {
varCache.Store(name, v)
return nil
}
return errors.New(str.Join("cache ", name, " type err"))
}
func NewVarCache[T any](c cache.AnyCache[T], fn func(context.Context, ...any) (T, error), a ...any) *cache.VarCache[T] {
inc := helper.ParseArgs((*cache.IncreaseUpdateVar[T])(nil), a...)
ref := helper.ParseArgs(cache.RefreshVar[T](nil), a...)
v := cache.NewVarCache(c, fn, inc, ref, a...)
name, _ := parseArgs(a...)
if name != "" {
varCache.Store(name, v)
}
PushOrSetFlush(mockmap.Item[string, Fn]{
Name: name,
Value: v.Flush,
})
cc, ok := any(c).(clearExpired)
if ok {
PushOrSetClearExpired(mockmap.Item[string, Fn]{
Name: name,
Value: cc.ClearExpired,
})
}
return v
}
func GetVarVal[T any](name string, ctx context.Context, duration time.Duration, a ...any) (r T, err error) {
ctx = context.WithValue(ctx, "getCache", name)
ca, ok := GetVarCache[T](name)
if !ok {
err = errors.New(str.Join("cache ", name, " is not exist"))
return
}
v, err := ca.GetCache(ctx, duration, a...)
if err != nil {
return
}
r = v
return
}
func NewVarMemoryCache[T any](fn func(context.Context, ...any) (T, error), expired time.Duration, a ...any) *cache.VarCache[T] {
c := cache.NewVarMemoryCache[T](nil)
name, e := parseArgs(a...)
SetExpireTime(c, name, expired, e)
v := NewVarCache[T](c, fn, a...)
return v
}
func GetVarCache[T any](name string) (*cache.VarCache[T], bool) {
v, ok := varCache.Load(name)
if !ok {
return nil, false
}
vv, ok := v.(*cache.VarCache[T])
return vv, ok
}

78
cache/locks.go vendored
View File

@ -1,78 +0,0 @@
package cache
import (
"context"
"github.com/fthvgb1/wp-go/safety"
"sync"
"sync/atomic"
)
type Locks[K comparable] struct {
numFn func() int
locks []*sync.Mutex
m *safety.Map[K, *sync.Mutex]
counter *int64
}
func (l *Locks[K]) Flush(_ context.Context) {
l.m.Flush()
atomic.StoreInt64(l.counter, 0)
}
func (l *Locks[K]) SetNumFn(numFn func() int) {
l.numFn = numFn
}
func NewLocks[K comparable](num func() int) *Locks[K] {
var i int64
return &Locks[K]{numFn: num, m: safety.NewMap[K, *sync.Mutex](), counter: &i}
}
func (l *Locks[K]) SetLockNum(num int) {
if num > 0 {
l.locks = make([]*sync.Mutex, num)
for i := 0; i < num; i++ {
l.locks[i] = &sync.Mutex{}
}
}
}
func (l *Locks[K]) GetLock(ctx context.Context, gMut *sync.Mutex, keys ...K) *sync.Mutex {
k := keys[0]
lo, ok := l.m.Load(k)
if ok {
return lo
}
num := l.numFn()
if num == 1 {
return gMut
}
gMut.Lock()
defer gMut.Unlock()
lo, ok = l.m.Load(k)
if ok {
return lo
}
if num <= 0 {
lo = &sync.Mutex{}
l.m.Store(k, lo)
return lo
}
if len(l.locks) == 0 {
l.SetLockNum(num)
}
counter := int(atomic.LoadInt64(l.counter))
if counter > len(l.locks)-1 {
atomic.StoreInt64(l.counter, 0)
counter = 0
}
lo = l.locks[counter]
l.m.Store(k, lo)
atomic.AddInt64(l.counter, 1)
if len(l.locks) < num {
for i := 0; i < num-len(l.locks); i++ {
l.locks = append(l.locks, &sync.Mutex{})
}
}
return lo
}

589
cache/map.go vendored
View File

@ -4,562 +4,172 @@ import (
"context"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/cache/reload"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/maps"
"github.com/fthvgb1/wp-go/helper/slice"
"sync"
"time"
)
type MapCache[K comparable, V any] struct {
Cache[K, V]
mux *sync.Mutex
muFn func(ctx context.Context, gMut *sync.Mutex, k ...K) *sync.Mutex
cacheFunc MapSingleFn[K, V]
batchCacheFn MapBatchFn[K, V]
getCacheBatch func(c context.Context, key []K, timeout time.Duration, params ...any) ([]V, error)
getCacheBatchToMap func(c context.Context, key []K, timeout time.Duration, params ...any) (map[K]V, error)
increaseUpdate *IncreaseUpdate[K, V]
refresh Refresh[K, V]
gets func(ctx context.Context, key K) (V, bool)
sets func(ctx context.Context, key K, val V)
getExpireTimes func(ctx context.Context) time.Duration
ttl func(ctx context.Context, key K) time.Duration
flush func(ctx context.Context)
del func(ctx context.Context, key ...K)
clearExpired func(ctx context.Context)
data Cache[K, V]
mux sync.Mutex
cacheFunc func(...any) (V, error)
batchCacheFn func(...any) (map[K]V, error)
expireTime time.Duration
}
func (m *MapCache[K, V]) Get(ctx context.Context, key K) (V, bool) {
return m.gets(ctx, key)
}
func (m *MapCache[K, V]) Set(ctx context.Context, key K, val V) {
m.sets(ctx, key, val)
}
func (m *MapCache[K, V]) Ttl(ctx context.Context, key K) time.Duration {
return m.ttl(ctx, key)
}
func (m *MapCache[K, V]) GetExpireTime(ctx context.Context) time.Duration {
return m.getExpireTimes(ctx)
}
func (m *MapCache[K, V]) Del(ctx context.Context, key ...K) {
m.del(ctx, key...)
}
func (m *MapCache[K, V]) ClearExpired(ctx context.Context) {
m.clearExpired(ctx)
}
type IncreaseUpdate[K comparable, V any] struct {
CycleTime func() time.Duration
Fn IncreaseFn[K, V]
}
func NewIncreaseUpdate[K comparable, V any](name string, fn IncreaseFn[K, V], cycleTime time.Duration, tFn func() time.Duration) *IncreaseUpdate[K, V] {
tFn = reload.BuildFnVal(name, cycleTime, tFn)
return &IncreaseUpdate[K, V]{CycleTime: tFn, Fn: fn}
}
type MapSingleFn[K, V any] func(context.Context, K, ...any) (V, error)
type MapBatchFn[K comparable, V any] func(context.Context, []K, ...any) (map[K]V, error)
type IncreaseFn[K comparable, V any] func(c context.Context, currentData V, k K, t time.Time, a ...any) (data V, save bool, refresh bool, err error)
func NewMapCache[K comparable, V any](ca Cache[K, V], cacheFunc MapSingleFn[K, V], batchCacheFn MapBatchFn[K, V], inc *IncreaseUpdate[K, V], lockFn LockFn[K], a ...any) *MapCache[K, V] {
r := &MapCache[K, V]{
Cache: ca,
mux: &sync.Mutex{},
cacheFunc: cacheFunc,
batchCacheFn: batchCacheFn,
increaseUpdate: inc,
muFn: lockFn,
}
if cacheFunc == nil && batchCacheFn != nil {
r.setDefaultCacheFn(batchCacheFn)
} else if batchCacheFn == nil && cacheFunc != nil {
r.SetDefaultBatchFunc(cacheFunc)
}
ex, ok := any(ca).(Expend[K, V])
if !ok {
r.getCacheBatch = r.getCacheBatchs
r.getCacheBatchToMap = r.getBatchToMapes
} else {
r.getCacheBatch = r.getBatches(ex)
r.getCacheBatchToMap = r.getBatchToMap(ex)
}
re, ok := any(ca).(Refresh[K, V])
if ok {
r.refresh = re
}
initCache(r, a...)
return r
}
func initCache[K comparable, V any](r *MapCache[K, V], a ...any) {
gets := helper.ParseArgs[func(Cache[K, V], context.Context, K) (V, bool)](nil, a...)
if gets == nil {
r.gets = r.Cache.Get
} else {
r.gets = func(ctx context.Context, key K) (V, bool) {
return gets(r.Cache, ctx, key)
}
}
sets := helper.ParseArgs[func(Cache[K, V], context.Context, K, V)](nil, a...)
if sets == nil {
r.sets = r.Cache.Set
} else {
r.sets = func(ctx context.Context, key K, val V) {
sets(r.Cache, ctx, key, val)
}
}
getExpireTimes := helper.ParseArgs[func(Cache[K, V], context.Context) time.Duration](nil, a...)
if getExpireTimes == nil {
r.getExpireTimes = r.Cache.GetExpireTime
} else {
r.getExpireTimes = func(ctx context.Context) time.Duration {
return getExpireTimes(r.Cache, ctx)
}
}
ttl := helper.ParseArgs[func(Cache[K, V], context.Context, K) time.Duration](nil, a...)
if ttl == nil {
r.ttl = r.Cache.Ttl
} else {
r.ttl = func(ctx context.Context, k K) time.Duration {
return ttl(r.Cache, ctx, k)
}
}
del := helper.ParseArgs[func(Cache[K, V], context.Context, ...K)](nil, a...)
if del == nil {
r.del = r.Cache.Del
} else {
r.del = func(ctx context.Context, key ...K) {
del(r.Cache, ctx, key...)
}
}
flushAndClearExpired := helper.ParseArgs[[]func(Cache[K, V], context.Context)](nil, a...)
if flushAndClearExpired == nil {
r.flush = r.Cache.Flush
r.clearExpired = r.Cache.ClearExpired
} else {
r.flush = func(ctx context.Context) {
flushAndClearExpired[0](r.Cache, ctx)
}
if len(flushAndClearExpired) > 1 {
r.clearExpired = func(ctx context.Context) {
flushAndClearExpired[1](r.Cache, ctx)
}
} else {
r.clearExpired = r.Cache.ClearExpired
}
}
}
func (m *MapCache[K, V]) SetDefaultBatchFunc(fn MapSingleFn[K, V]) {
m.batchCacheFn = func(ctx context.Context, ids []K, a ...any) (map[K]V, error) {
var err error
rr := make(map[K]V)
for _, id := range ids {
v, er := fn(ctx, id, a...)
if er != nil {
err = errors.Join(er)
continue
}
rr[id] = v
}
return rr, err
}
}
func (m *MapCache[K, V]) SetCacheFunc(fn MapSingleFn[K, V]) {
func (m *MapCache[K, V]) SetCacheFunc(fn func(...any) (V, error)) {
m.cacheFunc = fn
}
func (m *MapCache[K, V]) Ttl(ctx context.Context, k K) time.Duration {
return m.data.Ttl(ctx, k, m.expireTime)
}
func (m *MapCache[K, V]) GetLastSetTime(ctx context.Context, k K) (t time.Time) {
tt := m.Ttl(ctx, k)
tt := m.data.Ttl(ctx, k, m.expireTime)
if tt <= 0 {
return
}
return time.Now().Add(m.Ttl(ctx, k)).Add(-m.GetExpireTime(ctx))
return time.Now().Add(m.data.Ttl(ctx, k, m.expireTime)).Add(-m.expireTime)
}
func (m *MapCache[K, V]) SetCacheBatchFn(fn MapBatchFn[K, V]) {
func (m *MapCache[K, V]) SetCacheBatchFn(fn func(...any) (map[K]V, error)) {
m.batchCacheFn = fn
if m.cacheFunc == nil {
m.setDefaultCacheFn(fn)
m.setCacheFn(fn)
}
}
func (m *MapCache[K, V]) setDefaultCacheFn(fn MapBatchFn[K, V]) {
m.cacheFunc = func(ctx context.Context, k K, a ...any) (V, error) {
func (m *MapCache[K, V]) setCacheFn(fn func(...any) (map[K]V, error)) {
m.cacheFunc = func(a ...any) (V, error) {
var err error
var r map[K]V
r, err = fn(ctx, []K{k}, a...)
var id K
ctx, ok := a[0].(context.Context)
if ok {
id = a[1].(K)
r, err = fn(ctx, []K{id})
} else {
id = a[0].(K)
r, err = fn([]K{id})
}
if err != nil {
var rr V
return rr, err
}
return r[k], err
return r[id], err
}
}
func NewMapCacheByFn[K comparable, V any](cacheType Cache[K, V], fn func(...any) (V, error), expireTime time.Duration) *MapCache[K, V] {
return &MapCache[K, V]{
mux: sync.Mutex{},
cacheFunc: fn,
expireTime: expireTime,
data: cacheType,
}
}
func NewMapCacheByBatchFn[K comparable, V any](cacheType Cache[K, V], fn func(...any) (map[K]V, error), expireTime time.Duration) *MapCache[K, V] {
r := &MapCache[K, V]{
mux: sync.Mutex{},
batchCacheFn: fn,
expireTime: expireTime,
data: cacheType,
}
r.setCacheFn(fn)
return r
}
func (m *MapCache[K, V]) Flush(ctx context.Context) {
m.mux.Lock()
defer m.mux.Unlock()
m.flush(ctx)
m.data.Flush(ctx)
}
func (m *MapCache[K, V]) increaseUpdates(c context.Context, timeout time.Duration, data V, key K, params ...any) (V, error) {
var err error
nowTime := time.Now()
if nowTime.Sub(m.GetLastSetTime(c, key)) < m.increaseUpdate.CycleTime() {
return data, err
}
fn := func() {
l := m.muFn(c, m.mux, key)
l.Lock()
defer l.Unlock()
if nowTime.Sub(m.GetLastSetTime(c, key)) < m.increaseUpdate.CycleTime() {
return
}
dat, save, refresh, er := m.increaseUpdate.Fn(c, data, key, m.GetLastSetTime(c, key), params...)
if er != nil {
err = er
return
}
if refresh {
m.refresh.Refresh(c, key, params...)
}
if save {
m.Set(c, key, dat)
data = dat
}
}
if timeout > 0 {
er := helper.RunFnWithTimeout(c, timeout, fn)
if err == nil && er != nil {
return data, fmt.Errorf("increateUpdate cache %v err:[%s]", key, er)
}
} else {
fn()
}
return data, err
func (m *MapCache[K, V]) Get(ctx context.Context, k K) (V, bool) {
return m.data.Get(ctx, k)
}
func (m *MapCache[K, V]) Set(ctx context.Context, k K, v V) {
m.data.Set(ctx, k, v, m.expireTime)
}
func (m *MapCache[K, V]) GetCache(c context.Context, key K, timeout time.Duration, params ...any) (V, error) {
data, ok := m.Get(c, key)
data, ok := m.data.Get(c, key)
var err error
if ok {
if m.increaseUpdate == nil || m.refresh == nil {
return data, err
}
return m.increaseUpdates(c, timeout, data, key, params...)
}
if !ok || m.data.Ttl(c, key, m.expireTime) <= 0 {
ver := m.data.Ver(c, key)
call := func() {
l := m.muFn(c, m.mux, key)
l.Lock()
defer l.Unlock()
if data, ok = m.Get(c, key); ok {
m.mux.Lock()
defer m.mux.Unlock()
if m.data.Ver(c, key) > ver {
data, _ = m.data.Get(c, key)
return
}
data, err = m.cacheFunc(c, key, params...)
data, err = m.cacheFunc(params...)
if err != nil {
return
}
m.Set(c, key, data)
}
if timeout > 0 {
er := helper.RunFnWithTimeout(c, timeout, call, fmt.Sprintf("get cache %v ", key))
if err == nil && er != nil {
err = er
ctx, cancel := context.WithTimeout(c, timeout)
defer cancel()
done := make(chan struct{}, 1)
go func() {
call()
done <- struct{}{}
}()
select {
case <-ctx.Done():
err = errors.New(fmt.Sprintf("get cache %v %s", key, ctx.Err().Error()))
case <-done:
}
} else {
call()
}
}
return data, err
}
func (m *MapCache[K, V]) GetCacheBatch(c context.Context, key []K, timeout time.Duration, params ...any) ([]V, error) {
return m.getCacheBatch(c, key, timeout, params...)
}
var res []V
ver := 0
needFlush := slice.FilterAndMap(key, func(k K) (r K, ok bool) {
if _, ok := m.data.Get(c, k); !ok {
return k, true
}
ver += m.data.Ver(c, k)
return
})
func (m *MapCache[K, V]) GetBatchToMap(c context.Context, key []K, timeout time.Duration, params ...any) (map[K]V, error) {
return m.getCacheBatchToMap(c, key, timeout, params...)
}
func (m *MapCache[K, V]) getBatchToMap(e Expend[K, V]) func(c context.Context, key []K, timeout time.Duration, params ...any) (map[K]V, error) {
return func(ctx context.Context, key []K, timeout time.Duration, params ...any) (map[K]V, error) {
var res map[K]V
var err error
mm, err := e.Gets(ctx, key)
if err != nil || len(key) == len(mm) {
return mm, err
}
var needIndex = make(map[K]int)
res = mm
var flushKeys []K
for i, k := range key {
_, ok := mm[k]
if !ok {
flushKeys = append(flushKeys, k)
needIndex[k] = i
}
}
if len(needFlush) > 0 {
call := func() {
m.mux.Lock()
defer m.mux.Unlock()
mmm, er := e.Gets(ctx, maps.FilterToSlice(needIndex, func(k K, v int) (K, bool) {
return k, true
}))
if er != nil {
err = er
return
}
for k, v := range mmm {
res[k] = v
delete(needIndex, k)
}
if len(needIndex) < 1 {
vers := slice.Reduce(needFlush, func(t K, r int) int {
return r + m.data.Ver(c, t)
}, 0)
if vers > ver {
return
}
r, er := m.batchCacheFn(ctx, maps.FilterToSlice(needIndex, func(k K, v int) (K, bool) {
return k, true
}), params...)
if er != nil {
err = er
return
}
e.Sets(ctx, r)
for k := range needIndex {
v, ok := r[k]
if ok {
res[k] = v
}
}
}
if timeout > 0 {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
done := make(chan struct{}, 1)
go func() {
call()
done <- struct{}{}
}()
select {
case <-ctx.Done():
err = errors.New(fmt.Sprintf("get cache %v %s", key, ctx.Err().Error()))
return nil, err
case <-done:
}
} else {
call()
}
return res, err
}
}
func (m *MapCache[K, V]) getBatchToMapes(c context.Context, key []K, timeout time.Duration, params ...any) (r map[K]V, err error) {
r = make(map[K]V)
var needIndex = make(map[K]int)
for i, k := range key {
v, ok := m.Get(c, k)
if !ok {
needIndex[k] = i
} else {
r[k] = v
}
}
if len(needIndex) < 1 {
return
}
call := func() {
l := m.muFn(c, m.mux, key...)
l.Lock()
defer l.Unlock()
needFlushs := maps.FilterToSlice(needIndex, func(k K, v int) (K, bool) {
vv, ok := m.Get(c, k)
if ok {
r[k] = vv
delete(needIndex, k)
return k, false
}
return k, true
})
if len(needFlushs) < 1 {
return
}
rr, er := m.batchCacheFn(c, needFlushs, params...)
if er != nil {
err = er
return
}
for k := range needIndex {
v, ok := rr[k]
if ok {
r[k] = v
}
m.Set(c, k, v)
}
}
if timeout > 0 {
ctx, cancel := context.WithTimeout(c, timeout)
defer cancel()
done := make(chan struct{}, 1)
go func() {
call()
done <- struct{}{}
}()
select {
case <-ctx.Done():
err = errors.New(fmt.Sprintf("get cache %v %s", key, ctx.Err().Error()))
return nil, err
case <-done:
}
} else {
call()
}
return
}
func (m *MapCache[K, V]) getCacheBatchs(c context.Context, key []K, timeout time.Duration, params ...any) ([]V, error) {
var res = make([]V, 0, len(key))
var needIndex = make(map[K]int)
for i, k := range key {
v, ok := m.Get(c, k)
if !ok {
needIndex[k] = i
}
res = append(res, v)
}
if len(needIndex) < 1 {
return res, nil
}
var err error
call := func() {
l := m.muFn(c, m.mux, key...)
l.Lock()
defer l.Unlock()
needFlushs := maps.FilterToSlice(needIndex, func(k K, v int) (K, bool) {
vv, ok := m.Get(c, k)
if ok {
res[needIndex[k]] = vv
delete(needIndex, k)
return k, false
}
return k, true
})
if len(needFlushs) < 1 {
return
}
r, er := m.batchCacheFn(c, needFlushs, params...)
if er != nil {
err = er
return
}
for k, i := range needIndex {
v, ok := r[k]
if ok {
res[i] = v
m.Set(c, k, v)
}
}
}
if timeout > 0 {
ctx, cancel := context.WithTimeout(c, timeout)
defer cancel()
done := make(chan struct{}, 1)
go func() {
call()
done <- struct{}{}
}()
select {
case <-ctx.Done():
err = errors.New(fmt.Sprintf("get cache %v %s", key, ctx.Err().Error()))
return nil, err
case <-done:
}
} else {
call()
}
return res, err
}
func (m *MapCache[K, V]) getBatches(e Expend[K, V]) func(ctx context.Context, key []K, timeout time.Duration, params ...any) ([]V, error) {
cc := e
return func(ctx context.Context, key []K, timeout time.Duration, params ...any) ([]V, error) {
var res = make([]V, 0, len(key))
var needIndex = make(map[K]int)
var err error
mm, err := cc.Gets(ctx, key)
r, er := m.batchCacheFn(params...)
if err != nil {
return nil, err
}
var flushKeys []K
for i, k := range key {
v, ok := mm[k]
if !ok {
flushKeys = append(flushKeys, k)
needIndex[k] = i
var vv V
v = vv
}
res = append(res, v)
}
if len(needIndex) < 1 {
return res, nil
}
call := func() {
m.mux.Lock()
defer m.mux.Unlock()
mmm, er := cc.Gets(ctx, maps.FilterToSlice(needIndex, func(k K, v int) (K, bool) {
return k, true
}))
if er != nil {
err = er
return
}
for k, v := range mmm {
res[needIndex[k]] = v
delete(needIndex, k)
}
if len(needIndex) < 1 {
return
}
r, er := m.batchCacheFn(ctx, maps.FilterToSlice(needIndex, func(k K, v int) (K, bool) {
return k, true
}), params...)
if er != nil {
err = er
return
}
cc.Sets(ctx, r)
for k, i := range needIndex {
v, ok := r[k]
if ok {
res[i] = v
}
for k, v := range r {
m.Set(c, k, v)
}
}
if timeout > 0 {
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, cancel := context.WithTimeout(c, timeout)
defer cancel()
done := make(chan struct{}, 1)
go func() {
@ -569,13 +179,20 @@ func (m *MapCache[K, V]) getBatches(e Expend[K, V]) func(ctx context.Context, ke
select {
case <-ctx.Done():
err = errors.New(fmt.Sprintf("get cache %v %s", key, ctx.Err().Error()))
return nil, err
case <-done:
}
} else {
call()
}
return res, err
}
res = slice.FilterAndMap(key, func(k K) (V, bool) {
return m.data.Get(c, k)
})
return res, err
}
func (m *MapCache[K, V]) ClearExpired(ctx context.Context) {
m.mux.Lock()
defer m.mux.Unlock()
m.data.ClearExpired(ctx, m.expireTime)
}

39
cache/map_test.go vendored
View File

@ -12,22 +12,27 @@ import (
)
var ca MapCache[string, string]
var fn MapSingleFn[string, string]
var batchFn MapBatchFn[string, string]
var fn func(a ...any) (string, error)
var batchFn func(a ...any) (map[string]string, error)
var ct context.Context
func init() {
fn = func(ctx context.Context, aa string, a ...any) (string, error) {
fn = func(a ...any) (string, error) {
aa := a[1].(string)
return strings.Repeat(aa, 2), nil
}
ct = context.Background()
batchFn = func(ctx context.Context, arr []string, a ...any) (map[string]string, error) {
batchFn = func(a ...any) (map[string]string, error) {
fmt.Println(a)
return slice.FilterAndToMap(arr, func(t string, _ int) (string, string, bool) {
arr := a[1].([]string)
return slice.FilterAndToMap(arr, func(t string) (string, string, bool) {
return t, strings.Repeat(t, 2), true
}), nil
}
ca = *NewMemoryMapCacheByFn[string, string](fn, time.Second*2)
ca.SetCacheBatchFn(batchFn)
_, _ = ca.GetCache(ct, "aa", time.Second, ct, "aa")
_, _ = ca.GetCache(ct, "bb", time.Second, ct, "bb")
}
func TestMapCache_ClearExpired(t *testing.T) {
type args struct {
@ -67,9 +72,7 @@ func TestMapCache_Flush(t *testing.T) {
m MapCache[K, V]
args args
}
ca := *NewMapCache[string, string](NewMemoryMapCache[string, string](func() time.Duration {
return time.Second
}), fn, nil, nil, nil)
ca := *NewMemoryMapCacheByFn[string, string](fn, time.Second)
_, _ = ca.GetCache(ct, "aa", time.Second, ct, "aa")
tests := []testCase[string, string]{
{
@ -290,7 +293,7 @@ func TestMapCache_Set(t *testing.T) {
func TestMapCache_SetCacheBatchFn(t *testing.T) {
type args[K comparable, V any] struct {
fn MapBatchFn[K, V]
fn func(...any) (map[K]V, error)
}
type testCase[K comparable, V any] struct {
name string
@ -312,19 +315,19 @@ func TestMapCache_SetCacheBatchFn(t *testing.T) {
}
func TestMapCache_SetCacheFunc(t *testing.T) {
type args[K comparable, V any] struct {
fn MapSingleFn[K, V]
type args[V any] struct {
fn func(...any) (V, error)
}
type testCase[K comparable, V any] struct {
name string
m MapCache[K, V]
args args[K, V]
args args[V]
}
tests := []testCase[string, string]{
{
name: "t1",
m: ca,
args: args[string, string]{fn: fn},
args: args[string]{fn: fn},
},
}
for _, tt := range tests {
@ -352,12 +355,12 @@ func TestMapCache_Ttl(t *testing.T) {
name: "t1",
m: ca,
args: args[string]{ct, "aa"},
want: ca.GetExpireTime(ct) - tx.Sub(txx),
want: ca.expireTime - tx.Sub(txx),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Printf("过期时间=%v \nttl=%v \n当前时间 =%v\n最后设置时间=%v\n当时时间-最后设置时间=%v ", ca.GetExpireTime(ct), ca.Ttl(ct, "aa"), tx, txx, tx.Sub(txx))
fmt.Printf("过期时间=%v \nttl=%v \n当前时间 =%v\n最后设置时间=%v\n当时时间-最后设置时间=%v ", ca.expireTime, ca.Ttl(ct, "aa"), tx, txx, tx.Sub(txx))
if got := tt.m.Ttl(tt.args.ct, tt.args.k); got != tt.want {
t.Errorf("Ttl() = %v, want %v", got, tt.want)
}
@ -367,7 +370,7 @@ func TestMapCache_Ttl(t *testing.T) {
func TestMapCache_setCacheFn(t *testing.T) {
type args[K comparable, V any] struct {
fn MapBatchFn[K, V]
fn func(...any) (map[K]V, error)
}
type testCase[K comparable, V any] struct {
name string
@ -384,7 +387,7 @@ func TestMapCache_setCacheFn(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ca.cacheFunc = nil
tt.m.setDefaultCacheFn(tt.args.fn)
tt.m.setCacheFn(tt.args.fn)
fmt.Println(ca.GetCache(ct, "xx", time.Second, ct, "xx"))
})
}

View File

@ -2,22 +2,37 @@ package cache
import (
"context"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/safety"
"sync"
"time"
)
type MemoryMapCache[K comparable, V any] struct {
*safety.Map[K, mapVal[V]]
expireTime func() time.Duration
}
func NewMemoryMapCache[K comparable, V any](expireTime func() time.Duration) *MemoryMapCache[K, V] {
return &MemoryMapCache[K, V]{
Map: safety.NewMap[K, mapVal[V]](),
func NewMemoryMapCacheByFn[K comparable, V any](fn func(...any) (V, error), expireTime time.Duration) *MapCache[K, V] {
return &MapCache[K, V]{
data: NewMemoryMapCache[K, V](),
cacheFunc: fn,
expireTime: expireTime,
mux: sync.Mutex{},
}
}
func NewMemoryMapCacheByBatchFn[K comparable, V any](fn func(...any) (map[K]V, error), expireTime time.Duration) *MapCache[K, V] {
r := &MapCache[K, V]{
data: NewMemoryMapCache[K, V](),
batchCacheFn: fn,
expireTime: expireTime,
mux: sync.Mutex{},
}
r.setCacheFn(fn)
return r
}
func NewMemoryMapCache[K comparable, V any]() *MemoryMapCache[K, V] {
return &MemoryMapCache[K, V]{Map: safety.NewMap[K, mapVal[V]]()}
}
type mapVal[T any] struct {
setTime time.Time
@ -25,28 +40,15 @@ type mapVal[T any] struct {
data T
}
func (m *MemoryMapCache[K, V]) SetExpiredTime(f func() time.Duration) {
m.expireTime = f
}
func (m *MemoryMapCache[K, V]) GetExpireTime(_ context.Context) time.Duration {
return m.expireTime()
}
func (m *MemoryMapCache[K, V]) Get(_ context.Context, key K) (r V, ok bool) {
v, ok := m.Load(key)
if !ok {
return
}
r = v.data
t := m.expireTime() - time.Now().Sub(v.setTime)
if t <= 0 {
ok = false
if ok {
return v.data, true
}
return
}
func (m *MemoryMapCache[K, V]) Set(_ context.Context, key K, val V) {
func (m *MemoryMapCache[K, V]) Set(_ context.Context, key K, val V, _ time.Duration) {
v, ok := m.Load(key)
t := time.Now()
if ok {
@ -63,12 +65,12 @@ func (m *MemoryMapCache[K, V]) Set(_ context.Context, key K, val V) {
m.Store(key, v)
}
func (m *MemoryMapCache[K, V]) Ttl(_ context.Context, key K) time.Duration {
func (m *MemoryMapCache[K, V]) Ttl(_ context.Context, key K, expire time.Duration) time.Duration {
v, ok := m.Load(key)
if !ok {
return time.Duration(-1)
}
return m.expireTime() - time.Now().Sub(v.setTime)
return expire - time.Now().Sub(v.setTime)
}
func (m *MemoryMapCache[K, V]) Ver(_ context.Context, key K) int {
@ -83,28 +85,17 @@ func (m *MemoryMapCache[K, V]) Flush(context.Context) {
m.Map.Flush()
}
func (m *MemoryMapCache[K, V]) Del(_ context.Context, keys ...K) {
for _, key := range keys {
func (m *MemoryMapCache[K, V]) Delete(_ context.Context, key K) {
m.Map.Delete(key)
}
}
func (m *MemoryMapCache[K, V]) ClearExpired(_ context.Context) {
func (m *MemoryMapCache[K, V]) ClearExpired(_ context.Context, expire time.Duration) {
now := time.Duration(time.Now().UnixNano())
m.Range(func(k K, v mapVal[V]) bool {
if now > time.Duration(v.setTime.UnixNano())+m.expireTime() {
if now > time.Duration(v.setTime.UnixNano())+expire {
m.Map.Delete(k)
}
return true
})
}
func (m *MemoryMapCache[K, V]) Refresh(_ context.Context, k K, a ...any) {
v, ok := m.Load(k)
if !ok {
return
}
t := helper.ParseArgs(time.Now(), a...)
v.setTime = t
m.Store(k, v)
}

View File

@ -15,7 +15,7 @@ var ttt time.Time
func init() {
ctx = context.Background()
mm = *NewMemoryMapCache[string, string](3 * time.Second)
mm = *NewMemoryMapCache[string, string]()
ttt = time.Now()
mm.Store("aa", mapVal[string]{
setTime: ttt,
@ -53,7 +53,7 @@ func TestMemoryMapCache_ClearExpired(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Println(tt.m)
tt.m.ClearExpired(tt.args.in0)
tt.m.ClearExpired(tt.args.in0, tt.args.expire)
time.Sleep(time.Second)
fmt.Println(tt.m)
})
@ -83,7 +83,7 @@ func TestMemoryMapCache_Delete(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Println(mm.Get(ctx, "aa"))
tt.m.Del(tt.args.in0, tt.args.key)
tt.m.Delete(tt.args.in0, tt.args.key)
fmt.Println(mm.Get(ctx, "aa"))
})
@ -111,7 +111,7 @@ func TestMemoryMapCache_Flush(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.m.Flush(tt.args.in0)
mm.Set(ctx, "aa", "xx")
mm.Set(ctx, "aa", "xx", time.Second)
fmt.Println(mm.Get(ctx, "aa"))
})
}
@ -180,7 +180,7 @@ func TestMemoryMapCache_Set(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.m.Set(tt.args.in0, tt.args.key, tt.args.val)
tt.m.Set(tt.args.in0, tt.args.key, tt.args.val, tt.args.in3)
fmt.Println(tt.m.Get(ctx, tt.args.key))
})
}
@ -209,7 +209,7 @@ func TestMemoryMapCache_Ttl(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.m.Ttl(tt.args.in0, tt.args.key); got != tt.want {
if got := tt.m.Ttl(tt.args.in0, tt.args.key, tt.args.expire); got != tt.want {
t.Errorf("Ttl() = %v, want %v", got, tt.want)
}
})
@ -227,7 +227,7 @@ func TestMemoryMapCache_Ver(t *testing.T) {
args args[K]
want int
}
mm.Set(ctx, "aa", "ff")
mm.Set(ctx, "aa", "ff", time.Second)
tests := []testCase[string, string]{
{
name: "t1",

186
cache/pagination.go vendored
View File

@ -1,186 +0,0 @@
package cache
import (
"context"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/number"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"strings"
"time"
)
type Pagination[K comparable, V any] struct {
*MapCache[string, helper.PaginationData[V]]
maxNum func() int
isSwitch *safety.Map[K, bool]
dbFn func(ctx context.Context, k K, page, limit, totalRaw int, a ...any) ([]V, int, error)
localFn func(ctx context.Context, data []V, k K, page, limit int, a ...any) ([]V, int, error)
batchFetchNum func() int
localKeyFn func(K K, a ...any) string
dbKeyFn func(K K, a ...any) string
name string
}
var switchDb = errors.New("switch Db")
type DbFn[K comparable, V any] func(ctx context.Context, k K, page, limit, totalRaw int, a ...any) ([]V, int, error)
type LocalFn[K comparable, V any] func(ctx context.Context, data []V, k K, page, limit int, a ...any) ([]V, int, error)
func (p *Pagination[K, V]) IsSwitchDB(k K) bool {
v, _ := p.isSwitch.Load(k)
return v == true
}
func NewPagination[K comparable, V any](m *MapCache[string, helper.PaginationData[V]], maxNum func() int,
dbFn DbFn[K, V], localFn LocalFn[K, V], dbKeyFn, localKeyFn func(K, ...any) string,
batchFetchNum func() int, name string) *Pagination[K, V] {
if dbKeyFn == nil {
dbKeyFn = func(k K, a ...any) string {
s := str.NewBuilder()
for _, v := range append([]any{k}, a...) {
s.Sprintf("%v|", v)
}
return strings.TrimRight(s.String(), "|")
}
}
if localKeyFn == nil {
localKeyFn = func(k K, a ...any) string {
return fmt.Sprintf("%v", k)
}
}
return &Pagination[K, V]{
MapCache: m,
maxNum: maxNum,
isSwitch: safety.NewMap[K, bool](),
dbFn: dbFn,
localFn: localFn,
batchFetchNum: batchFetchNum,
name: name,
dbKeyFn: dbKeyFn,
localKeyFn: localKeyFn,
}
}
func (p *Pagination[K, V]) Pagination(ctx context.Context, timeout time.Duration, k K, page, limit int, a ...any) ([]V, int, error) {
if is, _ := p.isSwitch.Load(k); is {
return p.paginationByDB(ctx, timeout, k, page, limit, 0, a...)
}
data, total, err := p.paginationByLocal(ctx, timeout, k, page, limit, a...)
if err != nil {
if errors.Is(err, switchDb) {
p.isSwitch.Store(k, true)
err = nil
return p.paginationByDB(ctx, timeout, k, page, limit, total, a...)
}
return nil, 0, err
}
return data, total, err
}
func (p *Pagination[K, V]) paginationByLocal(ctx context.Context, timeout time.Duration, k K, page, limit int, a ...any) ([]V, int, error) {
key := p.localKeyFn(k)
data, ok := p.Get(ctx, key)
if ok {
if p.increaseUpdate != nil && p.refresh != nil {
dat, err := p.increaseUpdates(ctx, timeout, data, key, a...)
if err != nil {
return nil, 0, err
}
if dat.TotalRaw >= p.maxNum() {
return nil, 0, switchDb
}
data = dat
}
return p.localFn(ctx, data.Data, k, page, limit, a...)
}
p.mux.Lock()
defer p.mux.Unlock()
data, ok = p.Get(ctx, key)
if ok {
return data.Data, data.TotalRaw, nil
}
batchNum := p.batchFetchNum()
da, totalRaw, err := p.fetchDb(ctx, timeout, k, 1, 0, 0, a...)
if err != nil {
return nil, 0, err
}
if totalRaw < 1 {
data.Data = nil
data.TotalRaw = 0
p.Set(ctx, key, data)
return nil, 0, nil
}
if totalRaw >= p.maxNum() {
return nil, totalRaw, switchDb
}
totalPage := number.DivideCeil(totalRaw, batchNum)
for i := 1; i <= totalPage; i++ {
daa, _, err := p.fetchDb(ctx, timeout, k, i, batchNum, totalRaw, a...)
if err != nil {
return nil, 0, err
}
da = append(da, daa...)
}
data.Data = da
data.TotalRaw = totalRaw
p.Set(ctx, key, data)
return p.localFn(ctx, data.Data, k, page, limit, a...)
}
func (p *Pagination[K, V]) dbGet(ctx context.Context, key string) (helper.PaginationData[V], bool) {
data, ok := p.Get(ctx, key)
if ok && p.increaseUpdate != nil && p.increaseUpdate.CycleTime() > p.GetExpireTime(ctx)-p.Ttl(ctx, key) {
return data, true
}
return data, false
}
func (p *Pagination[K, V]) paginationByDB(ctx context.Context, timeout time.Duration, k K, page, limit, totalRaw int, a ...any) ([]V, int, error) {
key := p.dbKeyFn(k, append([]any{page, limit}, a...)...)
data, ok := p.dbGet(ctx, key)
if ok {
return data.Data, data.TotalRaw, nil
}
p.mux.Lock()
defer p.mux.Unlock()
data, ok = p.dbGet(ctx, key)
if ok {
return data.Data, data.TotalRaw, nil
}
dat, total, err := p.fetchDb(ctx, timeout, k, page, limit, totalRaw, a...)
if err != nil {
return nil, 0, err
}
data.Data, data.TotalRaw = dat, total
p.Set(ctx, key, data)
return data.Data, data.TotalRaw, err
}
func (p *Pagination[K, V]) fetchDb(ctx context.Context, timeout time.Duration, k K, page, limit, totalRaw int, a ...any) ([]V, int, error) {
var data helper.PaginationData[V]
var err error
fn := func() {
da, total, er := p.dbFn(ctx, k, page, limit, totalRaw, a...)
if er != nil {
err = er
return
}
data.Data = da
data.TotalRaw = total
}
if timeout > 0 {
er := helper.RunFnWithTimeout(ctx, timeout, fn, fmt.Sprintf("fetch %s-[%v]-page[%d]-limit[%d] from db fail", p.name, k, page, limit))
if err == nil && er != nil {
err = er
}
} else {
fn()
}
return data.Data, data.TotalRaw, err
}

544
cache/reload/reload.go vendored
View File

@ -1,544 +0,0 @@
package reload
import (
"github.com/fthvgb1/wp-go/helper"
"github.com/fthvgb1/wp-go/helper/slice"
str "github.com/fthvgb1/wp-go/helper/strings"
"github.com/fthvgb1/wp-go/safety"
"sync"
"sync/atomic"
)
type Queue struct {
Fn func()
Order float64
Name string
AutoExec bool
Once bool
}
var mut = &sync.Mutex{}
func GetGlobeMutex() *sync.Mutex {
return mut
}
var reloadQueues = safety.NewSlice[Queue]()
var reloadQueueHookFns = safety.NewVar[[]func(queue Queue) (Queue, bool)](nil)
var setFnVal = safety.NewMap[string, any]()
func DeleteReloadQueue(names ...string) {
hooks := reloadQueueHookFns.Load()
for _, name := range names {
hooks = append(hooks, func(queue Queue) (Queue, bool) {
if name != queue.Name {
return queue, true
}
return queue, false
})
}
reloadQueueHookFns.Store(hooks)
}
func HookReloadQueue(fn func(queue Queue) (Queue, bool)) {
a := reloadQueueHookFns.Load()
a = append(a, fn)
reloadQueueHookFns.Store(a)
}
func GetReloadFn(name string) func() {
hookQueue()
i, queue := slice.SearchFirst(reloadQueues.Load(), func(queue Queue) bool {
return queue.Name == name
})
if i > -1 && queue.Fn != nil {
return queue.Fn
}
return nil
}
func hookQueue() {
hooks := reloadQueueHookFns.Load()
queues := reloadQueues.Load()
length := len(queues)
for _, hook := range hooks {
queues = slice.FilterAndMap(queues, hook)
}
if len(queues) != length {
reloadQueues.Store(queues)
}
reloadQueueHookFns.Flush()
}
type SafetyVar[T, A any] struct {
Val *safety.Var[Val[T]]
Mutex sync.Mutex
}
type Val[T any] struct {
V T
Ok bool
}
type SafetyMap[K comparable, V, A any] struct {
Val *safety.Map[K, V]
Mutex sync.Mutex
}
var safetyMaps = safety.NewMap[string, any]()
var safetyMapLock = sync.Mutex{}
var deleteMapFn = safety.NewMap[string, func(any)]()
// GetValMap can get stored map value with namespace which called BuildSafetyMap, BuildMapFnWithConfirm, BuildMapFn, BuildMapFnWithAnyParams
func GetValMap[K comparable, V any](namespace string) (*safety.Map[K, V], bool) {
m, ok := safetyMaps.Load(namespace)
if !ok {
return nil, false
}
v, ok := m.(*safety.Map[K, V])
return v, ok
}
func DeleteMapVal[T any](namespace string, key ...T) {
fn, ok := deleteMapFn.Load(namespace)
if !ok || len(key) < 1 {
return
}
fn(key)
}
func Reloads(namespaces ...string) {
mut.Lock()
defer mut.Unlock()
hookQueue()
queues := reloadQueues.Load()
for _, name := range namespaces {
i, queue := slice.SearchFirst(queues, func(queue Queue) bool {
return name == queue.Name
})
if i < 0 {
continue
}
queue.Fn()
if queue.Once {
slice.Delete(&queues, i)
reloadQueues.Store(queues)
}
}
}
// BuildMapFnWithConfirm same as BuildMapFn
func BuildMapFnWithConfirm[K comparable, V, A any](namespace string, fn func(A) (V, bool), a ...any) func(key K, args A) V {
m := BuildSafetyMap[K, V, A](namespace, a...)
return func(key K, a A) V {
v, ok := m.Val.Load(key)
if ok {
return v
}
m.Mutex.Lock()
defer m.Mutex.Unlock()
v, ok = m.Val.Load(key)
if ok {
return v
}
v, ok = fn(a)
if ok {
m.Val.Store(key, v)
}
return v
}
}
// BuildMapFn build given fn with a new fn which returned value can be saved and flushed when called Reload or Reloads
// with namespace
//
// if give a float then can be reloaded early or lately, more bigger more earlier
//
// if give a bool false will not flushed when called Reload, then can called GetValMap to flush manually
func BuildMapFn[K comparable, V, A any](namespace string, fn func(A) V, a ...any) func(key K, args A) V {
m := BuildSafetyMap[K, V, A](namespace, a...)
return func(key K, a A) V {
v, ok := m.Val.Load(key)
if ok {
return v
}
m.Mutex.Lock()
defer m.Mutex.Unlock()
v, ok = m.Val.Load(key)
if ok {
return v
}
v = fn(a)
m.Val.Store(key, v)
return v
}
}
// BuildMapFnWithAnyParams same as BuildMapFn use multiple params
func BuildMapFnWithAnyParams[K comparable, V any](namespace string, fn func(...any) V, a ...any) func(key K, a ...any) V {
m := BuildSafetyMap[K, V, any](namespace, a...)
return func(key K, a ...any) V {
v, ok := m.Val.Load(key)
if ok {
return v
}
m.Mutex.Lock()
defer m.Mutex.Unlock()
v, ok = m.Val.Load(key)
if ok {
return v
}
v = fn(a...)
m.Val.Store(key, v)
return v
}
}
func BuildSafetyMap[K comparable, V, A any](namespace string, args ...any) *SafetyMap[K, V, A] {
vv, ok := safetyMaps.Load(namespace)
var m *SafetyMap[K, V, A]
if ok {
m = vv.(*SafetyMap[K, V, A])
return m
}
safetyMapLock.Lock()
defer safetyMapLock.Unlock()
vv, ok = safetyMaps.Load(namespace)
if ok {
m = vv.(*SafetyMap[K, V, A])
return m
}
m = &SafetyMap[K, V, A]{safety.NewMap[K, V](), sync.Mutex{}}
args = append(args, namespace)
deleteMapFn.Store(namespace, func(a any) {
k, ok := a.([]K)
if !ok && len(k) > 0 {
return
}
for _, key := range k {
m.Val.Delete(key)
}
})
Append(m.Val.Flush, args...)
safetyMaps.Store(namespace, m)
return m
}
func GetAnyValMapBy[K comparable, V, A any](namespace string, key K, a A, fn func(A) (V, bool), args ...any) V {
m := BuildSafetyMap[K, V, A](namespace, args...)
v, ok := m.Val.Load(key)
if ok {
return v
}
m.Mutex.Lock()
defer m.Mutex.Unlock()
v, ok = m.Val.Load(key)
if ok {
return v
}
v, ok = fn(a)
if ok {
m.Val.Store(key, v)
}
return v
}
func BuildAnyVal[T, A any](namespace string, args ...any) *SafetyVar[T, A] {
var vv *SafetyVar[T, A]
vvv, ok := safetyMaps.Load(namespace)
if ok {
vv = vvv.(*SafetyVar[T, A])
}
safetyMapLock.Lock()
defer safetyMapLock.Unlock()
vvv, ok = safetyMaps.Load(namespace)
if ok {
vv = vvv.(*SafetyVar[T, A])
return vv
}
v := Val[T]{}
vv = &SafetyVar[T, A]{safety.NewVar(v), sync.Mutex{}}
args = append(args, namespace)
Append(vv.Val.Flush, args...)
safetyMaps.Store(namespace, vv)
return vv
}
func GetAnyValBys[T, A any](namespace string, a A, fn func(A) (T, bool), args ...any) T {
var vv = BuildAnyVal[T, A](namespace, args...)
v := vv.Val.Load()
if v.Ok {
return v.V
}
vv.Mutex.Lock()
defer vv.Mutex.Unlock()
v = vv.Val.Load()
if v.Ok {
return v.V
}
v.V, v.Ok = fn(a)
vv.Val.Store(v)
return v.V
}
// BuildValFnWithConfirm same as BuildValFn
//
// if give a int and value bigger than 1 will be a times which built fn called return false
func BuildValFnWithConfirm[T, A any](namespace string, fn func(A) (T, bool), args ...any) func(A) T {
var vv = BuildAnyVal[T, A](namespace, args...)
tryTimes := helper.ParseArgs(1, args...)
var counter int64
if tryTimes > 1 {
Append(func() {
atomic.StoreInt64(&counter, 0)
}, str.Join("reload-valFn-counter-", namespace))
}
return func(a A) T {
v := vv.Val.Load()
if v.Ok {
return v.V
}
vv.Mutex.Lock()
defer vv.Mutex.Unlock()
v = vv.Val.Load()
if v.Ok {
return v.V
}
v.V, v.Ok = fn(a)
if v.Ok {
vv.Val.Store(v)
return v.V
}
if atomic.LoadInt64(&counter) <= 1 {
return v.V
}
atomic.AddInt64(&counter, 1)
if atomic.LoadInt64(&counter) >= int64(tryTimes) {
v.Ok = true
vv.Val.Store(v)
}
return v.V
}
}
// BuildValFn build given fn a new fn which return value can be saved and flushed when called Reload or Reloads
// with namespace.
//
// note: namespace should be not same as BuildMapFn and related fn, they stored same safety.Map[string,any].
//
// if give a float then can be reloaded early or lately, more bigger more earlier
//
// if give a bool false will not flushed when called Reload, but can call GetValMap or Reloads to flush manually
func BuildValFn[T, A any](namespace string, fn func(A) T, args ...any) func(A) T {
var vv = BuildAnyVal[T, A](namespace, args...)
return func(a A) T {
v := vv.Val.Load()
if v.Ok {
return v.V
}
vv.Mutex.Lock()
defer vv.Mutex.Unlock()
v = vv.Val.Load()
if v.Ok {
return v.V
}
v.V = fn(a)
v.Ok = true
vv.Val.Store(v)
return v.V
}
}
// BuildValFnWithAnyParams same as BuildValFn use multiple params
func BuildValFnWithAnyParams[T any](namespace string, fn func(...any) T, args ...any) func(...any) T {
var vv = BuildAnyVal[T, any](namespace, args...)
return func(a ...any) T {
v := vv.Val.Load()
if v.Ok {
return v.V
}
vv.Mutex.Lock()
defer vv.Mutex.Unlock()
v = vv.Val.Load()
if v.Ok {
return v.V
}
v.V = fn(a...)
v.Ok = true
vv.Val.Store(v)
return v.V
}
}
// Vars get default value and whenever reloaded assign default value
//
// args same as Append
//
// if give a name, then can be flushed by calls Reloads
//
// if give a float then can be reloaded early or lately, more bigger more earlier
//
// if give a bool false will not flushed when called Reload, but can call GetValMap or Reloads to flush manually
//
// if give a int 1 will only execute once when called Reload or Reloads and then delete the flush fn
func Vars[T any](defaults T, args ...any) *safety.Var[T] {
ss := safety.NewVar(defaults)
Append(func() {
ss.Store(defaults)
}, args...)
return ss
}
func parseArgs(a ...any) (ord float64, name string) {
if len(a) > 0 {
for _, arg := range a {
v, ok := arg.(float64)
if ok {
ord = v
}
vv, ok := arg.(string)
if ok {
name = vv
}
}
}
return ord, name
}
// VarsBy
//
// args same as Append
// if give a name, then can be flushed by calls Reloads
func VarsBy[T any](fn func() T, args ...any) *safety.Var[T] {
ss := safety.NewVar(fn())
Append(func() {
ss.Store(fn())
}, args...)
return ss
}
func MapBy[K comparable, T any](fn func(*safety.Map[K, T]), args ...any) *safety.Map[K, T] {
m := safety.NewMap[K, T]()
if fn != nil {
fn(m)
}
Append(func() {
m.Flush()
if fn != nil {
fn(m)
}
}, args...)
return m
}
func SafeMap[K comparable, T any](args ...any) *safety.Map[K, T] {
m := safety.NewMap[K, T]()
Append(m.Flush, args...)
return m
}
// Append the func that will be called whenever Reload called
//
// if give a name, then can be called by called Reloads
//
// if give a float then can be called early or lately when called Reload, more bigger more earlier
//
// if give a bool false will not execute when called Reload, then can called Reloads to execute manually
//
// if give a int 1 will only execute once when called Reload or Reloads and then delete the Queue
func Append(fn func(), a ...any) {
ord, name := parseArgs(a...)
autoExec := helper.ParseArgs(true, a...)
once := helper.ParseArgs(0, a...)
queues := reloadQueues.Load()
queue := Queue{fn, ord, name, autoExec, once == 1}
if name != "" {
i, _ := slice.SearchFirst(queues, func(queue Queue) bool {
return queue.Name == name
})
if i > -1 {
queues[i] = queue
reloadQueues.Store(queues)
return
}
}
reloadQueues.Append(queue)
}
// AppendOnceFn function and args same as Append, but func will execute only once when called Reload or Reloads and then will be deleted. Especially suitable for using to develop plugins, when uninstall plugin can clean or recover some progress's self relative data or behavior which was changed by plugin.
func AppendOnceFn(fn func(), a ...any) {
a = append([]any{1}, a...)
Append(fn, a...)
}
func Reload() {
mut.Lock()
defer mut.Unlock()
deleteMapFn.Flush()
hookQueue()
queues := reloadQueues.Load()
length := len(queues)
slice.SimpleSort(queues, slice.DESC, func(t Queue) float64 {
return t.Order
})
for i, queue := range queues {
if !queue.AutoExec {
continue
}
queue.Fn()
if queue.Once {
slice.Delete(&queues, i)
}
}
if length != len(queues) {
reloadQueues.Store(queues)
}
}
type Any[T any] struct {
fn func() T
v *safety.Var[T]
isManual *safety.Var[bool]
}
// BuildFnVal build a new fn which can be set value by SetFnVal with name or set default value by given fn when called Reload
func BuildFnVal[T any](name string, t T, fn func() T) func() T {
if fn == nil {
fn = func() T {
return t
}
} else {
t = fn()
}
p := safety.NewVar(t)
e := Any[T]{
fn: fn,
v: p,
isManual: safety.NewVar(false),
}
Append(func() {
if !e.isManual.Load() {
e.v.Store(fn())
}
}, str.Join("fnval-", name))
setFnVal.Store(name, e)
return func() T {
return e.v.Load()
}
}
func SetFnVal[T any](name string, val T, onlyManual bool) {
v, ok := setFnVal.Load(name)
if !ok {
return
}
vv, ok := v.(Any[T])
if !ok {
return
}
if onlyManual && !vv.isManual.Load() {
vv.isManual.Store(true)
}
vv.v.Store(val)
}

View File

@ -1,47 +0,0 @@
package reload
import (
"fmt"
"testing"
)
func TestFlushMapVal(t *testing.T) {
t.Run("t1", func(t *testing.T) {
c := 0
v := GetAnyValMapBy("key", 2, struct{}{}, func(a struct{}) (int, bool) {
c++
return 33, true
})
fmt.Println(v)
DeleteMapVal("key", 2)
v = GetAnyValMapBy("key", 2, struct{}{}, func(a struct{}) (int, bool) {
fmt.Println("xxxxx")
return 33, true
})
fmt.Println(v)
Reloads("key")
v = GetAnyValMapBy[int, int, struct{}]("key", 2, struct{}{}, func(a struct{}) (int, bool) {
fmt.Println("yyyy")
return 33, true
})
fmt.Println(v)
})
}
func TestGetAnyMapFnBys(t *testing.T) {
var i int
t.Run("t1", func(t *testing.T) {
v := BuildMapFnWithConfirm[int]("name", func(a int) (int, bool) {
i++
return a + 1, true
})
vv := v(1, 2)
vvv := v(2, 3)
fmt.Println(vv, vvv)
v(1, 2)
DeleteMapVal("name", 2)
v(2, 3)
fmt.Println(i)
})
}

251
cache/vars.go vendored
View File

@ -2,197 +2,98 @@ package cache
import (
"context"
"github.com/fthvgb1/wp-go/helper"
"errors"
"fmt"
"github.com/fthvgb1/wp-go/safety"
"sync"
"time"
)
type VarCache[T any] struct {
AnyCache[T]
setCacheFunc func(context.Context, ...any) (T, error)
mutex sync.Mutex
increaseUpdate *IncreaseUpdateVar[T]
refresh RefreshVar[T]
get func(ctx context.Context) (T, bool)
set func(ctx context.Context, v T)
flush func(ctx context.Context)
getLastSetTime func(ctx context.Context) time.Time
}
type IncreaseUpdateVar[T any] struct {
CycleTime func() time.Duration
Fn IncreaseVarFn[T]
}
type IncreaseVarFn[T any] func(c context.Context, currentData T, t time.Time, a ...any) (data T, save bool, refresh bool, err error)
func (t *VarCache[T]) Get(ctx context.Context) (T, bool) {
return t.get(ctx)
}
func (t *VarCache[T]) Set(ctx context.Context, v T) {
t.set(ctx, v)
}
func (t *VarCache[T]) Flush(ctx context.Context) {
t.flush(ctx)
}
func (t *VarCache[T]) GetLastSetTime(ctx context.Context) time.Time {
return t.getLastSetTime(ctx)
}
func initVarCache[T any](t *VarCache[T], a ...any) {
gets := helper.ParseArgs[func(AnyCache[T], context.Context) (T, bool)](nil, a...)
if gets == nil {
t.get = t.AnyCache.Get
} else {
t.get = func(ctx context.Context) (T, bool) {
return gets(t.AnyCache, ctx)
}
}
set := helper.ParseArgs[func(AnyCache[T], context.Context, T)](nil, a...)
if set == nil {
t.set = t.AnyCache.Set
} else {
t.set = func(ctx context.Context, v T) {
set(t.AnyCache, ctx, v)
}
}
flush := helper.ParseArgs[func(AnyCache[T], context.Context)](nil, a...)
if flush == nil {
t.flush = t.AnyCache.Flush
} else {
t.flush = func(ctx context.Context) {
flush(t.AnyCache, ctx)
}
}
getLastSetTime := helper.ParseArgs[func(AnyCache[T], context.Context) time.Time](nil, a...)
if getLastSetTime == nil {
t.getLastSetTime = t.AnyCache.GetLastSetTime
} else {
t.getLastSetTime = func(ctx context.Context) time.Time {
return getLastSetTime(t.AnyCache, ctx)
}
}
}
func NewVarCache[T any](cache AnyCache[T], fn func(context.Context, ...any) (T, error), inc *IncreaseUpdateVar[T], ref RefreshVar[T], a ...any) *VarCache[T] {
r := &VarCache[T]{
AnyCache: cache, setCacheFunc: fn, mutex: sync.Mutex{},
increaseUpdate: inc,
refresh: ref,
}
initVarCache(r, a...)
return r
}
func (t *VarCache[T]) GetCache(ctx context.Context, timeout time.Duration, params ...any) (T, error) {
data, ok := t.Get(ctx)
var err error
if ok {
if t.increaseUpdate != nil && t.refresh != nil {
nowTime := time.Now()
if t.increaseUpdate.CycleTime() > nowTime.Sub(t.GetLastSetTime(ctx)) {
return data, nil
}
fn := func() {
t.mutex.Lock()
defer t.mutex.Unlock()
da, save, refresh, er := t.increaseUpdate.Fn(ctx, data, t.GetLastSetTime(ctx), params...)
if er != nil {
err = er
return
}
if save {
t.Set(ctx, da)
}
if refresh {
t.refresh.Refresh(ctx, params...)
}
}
if timeout > 0 {
er := helper.RunFnWithTimeout(ctx, timeout, fn, "increaseUpdate cache fail")
if err == nil && er != nil {
err = er
}
} else {
fn()
}
}
return data, nil
}
call := func() {
t.mutex.Lock()
defer t.mutex.Unlock()
dat, ok := t.Get(ctx)
if ok {
data = dat
return
}
r, er := t.setCacheFunc(ctx, params...)
if er != nil {
err = er
return
}
t.Set(ctx, r)
data = r
}
if timeout > 0 {
er := helper.RunFnWithTimeout(ctx, timeout, call, "get cache fail")
if err == nil && er != nil {
err = er
}
} else {
call()
}
return data, err
}
type VarMemoryCache[T any] struct {
v *safety.Var[vars[T]]
expireTime func() time.Duration
}
func (c *VarMemoryCache[T]) ClearExpired(ctx context.Context) {
_, ok := c.Get(ctx)
if !ok {
c.Flush(ctx)
}
}
func NewVarMemoryCache[T any](expireTime func() time.Duration) *VarMemoryCache[T] {
return &VarMemoryCache[T]{v: safety.NewVar(vars[T]{}), expireTime: expireTime}
}
func (c *VarMemoryCache[T]) Get(_ context.Context) (T, bool) {
v := c.v.Load()
return v.data, c.expireTime() >= time.Now().Sub(v.setTime)
}
func (c *VarMemoryCache[T]) Set(_ context.Context, v T) {
vv := c.v.Load()
vv.data = v
vv.setTime = time.Now()
vv.incr++
c.v.Store(vv)
}
func (c *VarMemoryCache[T]) SetExpiredTime(f func() time.Duration) {
c.expireTime = f
}
type vars[T any] struct {
data T
mutex *sync.Mutex
setCacheFunc func(...any) (T, error)
expireTime time.Duration
setTime time.Time
incr int
}
func (c *VarMemoryCache[T]) GetLastSetTime(_ context.Context) time.Time {
func (c *VarCache[T]) GetLastSetTime() time.Time {
return c.v.Load().setTime
}
func (c *VarMemoryCache[T]) Flush(_ context.Context) {
c.v.Flush()
func NewVarCache[T any](fun func(...any) (T, error), duration time.Duration) *VarCache[T] {
return &VarCache[T]{
v: safety.NewVar(vars[T]{
mutex: &sync.Mutex{},
setCacheFunc: fun,
expireTime: duration,
}),
}
}
func (c *VarCache[T]) IsExpired() bool {
v := c.v.Load()
return time.Duration(v.setTime.UnixNano())+v.expireTime < time.Duration(time.Now().UnixNano())
}
func (c *VarCache[T]) Flush() {
v := c.v.Load()
mu := v.mutex
mu.Lock()
defer mu.Unlock()
var vv T
v.data = vv
c.v.Store(v)
}
func (c *VarCache[T]) GetCache(ctx context.Context, timeout time.Duration, params ...any) (T, error) {
v := c.v.Load()
data := v.data
var err error
if v.expireTime <= 0 || ((time.Duration(v.setTime.UnixNano()) + v.expireTime) < time.Duration(time.Now().UnixNano())) {
t := v.incr
call := func() {
v.mutex.Lock()
defer v.mutex.Unlock()
vv := c.v.Load()
if vv.incr > t {
return
}
r, er := vv.setCacheFunc(params...)
if err != nil {
err = er
return
}
vv.setTime = time.Now()
vv.data = r
data = r
vv.incr++
c.v.Store(vv)
}
if timeout > 0 {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
done := make(chan struct{}, 1)
go func() {
call()
done <- struct{}{}
close(done)
}()
select {
case <-ctx.Done():
err = errors.New(fmt.Sprintf("get cache %s", ctx.Err().Error()))
case <-done:
}
} else {
call()
}
}
return data, err
}

41
cache/vars_test.go vendored
View File

@ -7,11 +7,9 @@ import (
"time"
)
var cc = *NewVarCache[int](NewVarMemoryCache[int](func() time.Duration {
return time.Minute
}), func(ctx context.Context, a ...any) (int, error) {
var cc = *NewVarCache(func(a ...any) (int, error) {
return 1, nil
})
}, time.Minute)
func TestVarCache_Flush(t *testing.T) {
type testCase[T any] struct {
@ -28,8 +26,41 @@ func TestVarCache_Flush(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fmt.Println(tt.c.GetCache(c, time.Second))
tt.c.Flush(ctx)
tt.c.Flush()
fmt.Println(tt.c.GetCache(c, time.Second))
})
}
}
func TestVarCache_IsExpired(t *testing.T) {
type testCase[T any] struct {
name string
c VarCache[T]
want bool
}
tests := []testCase[int]{
{
name: "expired",
c: cc,
want: true,
},
{
name: "not expired",
c: func() VarCache[int] {
v := *NewVarCache(func(a ...any) (int, error) {
return 1, nil
}, time.Minute)
_, _ = v.GetCache(context.Background(), time.Second)
return v
}(),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.c.IsExpired(); got != tt.want {
t.Errorf("IsExpired() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,81 +0,0 @@
{
"mysql": {
"dsn": {
"host": "localhost",
"port": 3306,
"db": "wordpress",
"user": "root",
"password": "root",
"charset": "utf8mb4"
},
"pool": {
"connMaxIdleTime": 60,
"maxOpenConn": 100,
"maxIdleConn": 10,
"connMaxLifetime": 236
}
},
"Mail": {
"user": "xx@163.com",
"alias": "xx",
"pass": null,
"host": "smtp.163.com",
"port": 465,
"insecureSkipVerify": false
},
"ssl": {
"cert": "",
"key": ""
},
"cacheTime": {
"cacheControl": "72h",
"recentPostCacheTime": "5m",
"categoryCacheTime": "5m",
"contextPostCacheTime": "10h",
"recentCommentsCacheTime": "5m",
"digestCacheTime": "5m",
"postListCacheTime": "1h",
"searchPostCacheTime": "5m",
"monthPostCacheTime": "1h",
"postDataCacheTime": "1h",
"postCommentsCacheTime": "5m",
"crontabClearCacheTime": "5m",
"maxPostIdCacheTime": "1h",
"userInfoCacheTime": "24h",
"commentsCacheTime": "24h",
"sleepTime": [
"1s",
"3s"
]
},
"digestWordCount": 300,
"digestTag": "<a><b><blockquote><br><cite><code><dd><del><div><dl><dt><em><h1><h2><h3><h4><h5><h6><i><img><li><ol><p><pre><span><strong><ul>",
"maxRequestSleepNum": 100,
"maxRequestNum": 500,
"singleIpSearchNum": 10,
"logOutput": "err.log",
"gzip": false,
"postCommentUrl": "http://127.0.0.1/wp-comments-post.php",
"trustIps": [],
"paginationStep": 1,
"showQuerySql": false,
"trustServerNames": [
"xy.test",
"blog.xy.test"
],
"theme": "twentyfifteen",
"postOrder": "desc",
"wpDir": "/var/www/html/wordpress",
"pprof": "/debug/pprof",
"plugins": [
"enlightjs"
],
"pluginPath": "./plugins",
"listPagePlugins": [
"digest"
],
"externScript": [
"",
""
]
}

View File

@ -19,9 +19,9 @@ Mail:
user: xx@163.com
alias: xx
pass:
host: smtp.163.com
port: 465
insecureSkipVerify: false
Host: smtp.163.com
Port: 465
Ssl: true
ssl:
cert: ""
@ -58,55 +58,12 @@ cacheTime:
userInfoCacheTime: 24h
# 单独评论缓存时间
commentsCacheTime: 24h
# 评论增量更新时间
commentsIncreaseUpdateTime: 30s
# 主题的页眉图片缓存时间
themeHeaderImagCacheTime: 5m
# 随机sleep时间
sleepTime: [ 1s,3s ]
# 摘要字数 >0截取指定字数 =0输出出空字符 <0为不截取,原样输出
# 摘要字数
digestWordCount: 300
# 摘要允许的标签 默认为<a><b><blockquote><br><cite><code><dd><del><div><dl><dt><em><h1><h2><h3><h4><h5><h6><i><img><li><ol><p><pre><span><strong><ul>
digestTag: "<a><b><blockquote><br><cite><code><dd><del><div><dl><dt><em><h1><h2><h3><h4><h5><h6><i><img><li><ol><p><pre><span><strong><ul>"
# 设置html转义实体正则 the html coded character set regex file: plugin/digest/digest.go:12
#digestRegex: "&quot;*|&amp;*|&lt;*|&gt;*|&nbsp;*|&#91;*|&#93;*|&emsp;*"
# 可以设置每个标签或者转义字符占用的字数默认都为0 set tag or escape character occupied num, default every tag occupied 0
#digestTagOccupyNum: [
# {
# tag: "<top>", # 最外层固定tag outermost immovable tag
# num: 0,
# chuckOvered: false,
# escapeCharacter: [
# {
# character: [ "\n","\r","\t" ],
# num: 0
# },
# ]
# },{
# tag: "<img>",
# num: 1,
# chuckOvered: false
# },
# {
# tag: "<pre><code>",
# num: 0,
# escapeCharacter: [
# {
# character: ["\t"],
# num: 4,
# chuckOvered: false,
# },
# {
# character: ["\n","\r"],
# num: 1
# },
# {
# tags: "<br>",
# num: 1
# },
# ]
# },
#]
# 到达指定并发请求数时随机sleep
maxRequestSleepNum: 100
@ -114,33 +71,27 @@ maxRequestSleepNum: 100
maxRequestNum: 500
# 单ip同时最大搜索请求数
singleIpSearchNum: 10
# 错误日志输出路径 stdout|stderr|file path 默认为stderr
logOutput: err.log
# Gzip
gzip: false
# 提交评论url host需为ip形式
postCommentUrl: http://127.0.0.1/wp-comments-post.php
# 提交评论url
postCommentUrl: http://wp.test/wp-comments-post.php
# TrustIps
trustIps: [ ]
trustIps: []
# 分页器间隔数
paginationStep: 1
# 显示查询的sql语句
showQuerySql: false
# trust servername 信任的域名
trustServerNames: [ "xy.test","blog.xy.test" ]
trustServerNames: ["xy.test","blog.xy.test"]
# 主题 为空值为option template没有就默认为twentyfifteen
theme: "twentyfifteen"
# 文档排序默认升序还是降序
postOrder: "desc"
# WordPress path
wpDir: "/var/www/html/wordpress"
# 上传的目录
uploadDir: ""
# pprof route path 为空表示不开启pprof,否则为pprof的路由
pprof: "/debug/pprof"
# 要使用的程序插件名
plugins: [ "enlightjs" ]
# 插件存放路径
pluginPath: "./plugins"
# 列表页面post使用的插件
listPagePlugins: [ "digest" ]
# 额外引入的脚本 第一个为head 第二个为footer
externScript: [ "","" ]
listPagePlugins: ["passwordProject","digest","twentyseventeen_postThumbnail"]

67
go.mod
View File

@ -1,53 +1,42 @@
module github.com/fthvgb1/wp-go
go 1.21.0
toolchain go1.23.0
go 1.19
require (
github.com/dlclark/regexp2 v1.11.4
github.com/elliotchance/phpserialize v1.4.0
github.com/gin-contrib/gzip v1.0.1
github.com/gin-contrib/pprof v1.5.0
github.com/gin-contrib/sessions v1.0.1
github.com/gin-gonic/gin v1.10.0
github.com/go-playground/locales v0.14.1
github.com/go-playground/universal-translator v0.18.1
github.com/go-playground/validator/v10 v10.22.0
github.com/go-sql-driver/mysql v1.8.1
github.com/goccy/go-json v0.10.3
github.com/jmoiron/sqlx v1.4.0
golang.org/x/crypto v0.26.0
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
github.com/dlclark/regexp2 v1.7.0
github.com/elliotchance/phpserialize v1.3.3
github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/pprof v1.4.0
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.8.1
github.com/go-sql-driver/mysql v1.6.0
github.com/jmoiron/sqlx v1.3.5
github.com/soxfmr/gomail v0.0.0-20200806033254-80bf84e583f0
golang.org/x/crypto v0.0.0-20220924013350-4ba4fb4dd9e7
golang.org/x/exp v0.0.0-20230203172020-98cc5a0785f9
gopkg.in/yaml.v2 v2.4.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.12.2 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.3.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.1 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.9.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
)

Some files were not shown because too many files have changed in this diff Show More