From 7e73f95ff07ef114b5327fd25bbbd38393df8623 Mon Sep 17 00:00:00 2001 From: sunwei Date: Sat, 11 Jan 2025 12:27:40 +0800 Subject: [PATCH] support reserved structure --- ADRs/02.ContentDirectoryStructure.md | 2 + internal/domain/config/entity/config.go | 1 + internal/domain/config/entity/sitemap.go | 15 ++ internal/domain/config/factory/loader.go | 6 + internal/domain/config/factory/provider.go | 1 + internal/domain/config/valueobject/sitemap.go | 19 +++ internal/domain/content/entity/build.go | 8 +- internal/domain/content/entity/content.go | 4 +- internal/domain/content/repository/repo.go | 1 + internal/domain/content/valueobject/site.go | 14 +- .../domain/contenthub/entity/contenthub.go | 4 +- .../domain/contenthub/entity/pagefields.go | 9 ++ internal/domain/contenthub/entity/pagemap.go | 39 +++++ internal/domain/contenthub/type.go | 9 ++ .../domain/contenthub/valueobject/pagenop.go | 10 ++ .../domain/contenthub/valueobject/pager.go | 8 + .../contenthub/valueobject/paginator.go | 10 +- internal/domain/markdown/type.go | 5 + .../domain/markdown/valueobject/nodeheader.go | 21 +++ .../markdown/valueobject/nodeparagraph.go | 9 ++ .../markdown/valueobject/parserresult.go | 30 ++++ internal/domain/resources/entity/bundler.go | 144 ++++++++++++++++++ internal/domain/resources/entity/resources.go | 1 + internal/domain/resources/factory/resource.go | 2 + internal/domain/resources/type.go | 1 + internal/domain/site/entity/hugo.go | 4 + internal/domain/site/entity/pagefields.go | 17 +-- internal/domain/site/entity/paginator.go | 6 + internal/domain/site/entity/reserve.go | 33 ++++ internal/domain/site/entity/site.go | 2 + internal/domain/site/entity/sitefields.go | 52 ++++--- internal/domain/site/entity/siteinit.go | 4 +- internal/domain/site/factory/site.go | 2 + internal/domain/site/type.go | 8 +- internal/domain/site/valueobject/menu.go | 13 ++ internal/domain/site/valueobject/reserved.go | 3 +- .../embedded/templates/_default/sitemap.xml | 2 +- internal/interfaces/api/database/db.go | 20 +++ internal/interfaces/api/database/dbitem.go | 5 + internal/interfaces/cli/vercurr.go | 2 +- manifest.json | 2 +- pkg/db/content.go | 30 ++++ pkg/db/store.go | 5 + pkg/identity/identity.go | 7 + pkg/maps/params.go | 11 +- pkg/template/funcs/resource/resources.go | 26 ++++ pkg/template/funcs/resource/type.go | 2 + 47 files changed, 565 insertions(+), 64 deletions(-) create mode 100644 internal/domain/config/entity/sitemap.go create mode 100644 internal/domain/config/valueobject/sitemap.go create mode 100644 internal/domain/markdown/valueobject/nodeparagraph.go create mode 100644 internal/domain/resources/entity/bundler.go create mode 100644 internal/domain/site/entity/reserve.go diff --git a/ADRs/02.ContentDirectoryStructure.md b/ADRs/02.ContentDirectoryStructure.md index bb708a03..08eece4d 100644 --- a/ADRs/02.ContentDirectoryStructure.md +++ b/ADRs/02.ContentDirectoryStructure.md @@ -55,6 +55,8 @@ We will simplify the directory structure as follows: │ ├── 📄 index.md # About Home | 关于主页 │ ├── 📄 resume.md # Resume | 简历 │ ├── 📄 company.md # Company | 公司信息 +│ ├── 📄 contact.md # Contact | 联系方式 +│ ├── 📄 social.md # Social Media | 社交媒体 │ ├── 📁 portfolio # 🏆 Portfolio | 案例展示 │ ├── 📄 index.md # Portfolio Home | 案例主页 diff --git a/internal/domain/config/entity/config.go b/internal/domain/config/entity/config.go index 07a98476..fd7df3c6 100644 --- a/internal/domain/config/entity/config.go +++ b/internal/domain/config/entity/config.go @@ -24,6 +24,7 @@ type Config struct { OutputFormats MinifyC + Sitemap *Taxonomy } diff --git a/internal/domain/config/entity/sitemap.go b/internal/domain/config/entity/sitemap.go new file mode 100644 index 00000000..7395c79e --- /dev/null +++ b/internal/domain/config/entity/sitemap.go @@ -0,0 +1,15 @@ +package entity + +import "github.com/gohugonet/hugoverse/internal/domain/config/valueobject" + +type Sitemap struct { + Conf valueobject.SitemapConfig +} + +func (s Sitemap) ChangeFreq() string { + return s.Conf.ChangeFreq +} + +func (s Sitemap) Priority() float64 { + return s.Conf.Priority +} diff --git a/internal/domain/config/factory/loader.go b/internal/domain/config/factory/loader.go index e9ef3385..30598f16 100644 --- a/internal/domain/config/factory/loader.go +++ b/internal/domain/config/factory/loader.go @@ -220,6 +220,12 @@ func (cl *ConfigLoader) decodeConfig(p config.Provider, target *entity.Config) e } target.Module.ModuleConfig = m + sitemap, err := valueobject.DecodeSitemap(valueobject.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.GetStringMap("sitemap")) + if err != nil { + return err + } + target.Sitemap.Conf = sitemap + languages, err := valueobject.DecodeLanguageConfig(p) if err != nil { return err diff --git a/internal/domain/config/factory/provider.go b/internal/domain/config/factory/provider.go index 58bebf6f..7b8ed27d 100644 --- a/internal/domain/config/factory/provider.go +++ b/internal/domain/config/factory/provider.go @@ -59,6 +59,7 @@ func LoadConfig() (*entity.Config, error) { Language: &entity.Language{}, Imaging: entity.Imaging{}, MediaType: entity.MediaType{}, + Sitemap: entity.Sitemap{}, Taxonomy: &entity.Taxonomy{}, } diff --git a/internal/domain/config/valueobject/sitemap.go b/internal/domain/config/valueobject/sitemap.go new file mode 100644 index 00000000..b9065849 --- /dev/null +++ b/internal/domain/config/valueobject/sitemap.go @@ -0,0 +1,19 @@ +package valueobject + +import "github.com/mitchellh/mapstructure" + +type SitemapConfig struct { + // The page change frequency. + ChangeFreq string + // The priority of the page. + Priority float64 + // The sitemap filename. + Filename string + // Whether to disable page inclusion. + Disable bool +} + +func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) { + err := mapstructure.WeakDecode(input, &prototype) + return prototype, err +} diff --git a/internal/domain/content/entity/build.go b/internal/domain/content/entity/build.go index 47f8969f..4e6ac13a 100644 --- a/internal/domain/content/entity/build.go +++ b/internal/domain/content/entity/build.go @@ -75,14 +75,13 @@ func (c *Content) BuildTarget(contentType, id, status string) (string, error) { func (c *Content) writeSitePosts(siteId int, dir string, writerFiles chan *valueobject.File) error { q := fmt.Sprintf(`site%d`, siteId) - encodedQ := url.QueryEscape(q) - sitePosts, err := c.search("SitePost", fmt.Sprintf("slug:%s", encodedQ)) + sitePosts, err := c.Repo.ContentByPrefix(GetNamespace("SitePost", ""), q) if err != nil { return err } - c.Log.Printf("sitePosts len: %d", len(sitePosts)) + c.Log.Debugf("sitePosts len: %d", len(sitePosts)) for _, data := range sitePosts { var sp valueobject.SitePost @@ -117,9 +116,8 @@ func (c *Content) writeSitePosts(siteId int, dir string, writerFiles chan *value func (c *Content) writeSiteResource(siteId int, dir string) error { q := fmt.Sprintf(`site%d`, siteId) - encodedQ := url.QueryEscape(q) - siteResources, err := c.search("SiteResource", fmt.Sprintf("slug:%s", encodedQ)) + siteResources, err := c.Repo.ContentByPrefix(GetNamespace("SiteResource", ""), q) if err != nil { return err } diff --git a/internal/domain/content/entity/content.go b/internal/domain/content/entity/content.go index 2fcb727b..76ddd1bc 100644 --- a/internal/domain/content/entity/content.go +++ b/internal/domain/content/entity/content.go @@ -138,7 +138,7 @@ func (c *Content) UpdateContentObject(ci any) error { GetNamespace(cii.ItemName(), string(cis.ItemStatus())), fmt.Sprintf("%d", cii.ItemID()), b); err != nil { - log.Println("[search] UpdateIndex Error:", err) + c.Log.Errorln("[search] UpdateIndex Error:", err) } }() } @@ -147,7 +147,7 @@ func (c *Content) UpdateContentObject(ci any) error { go func() { err := c.SortContent(cii.ItemName()) if err != nil { - log.Println("sort content err: ", err) + c.Log.Errorln("sort content err: ", err) } }() } diff --git a/internal/domain/content/repository/repo.go b/internal/domain/content/repository/repo.go index 27e4cf52..60b59aef 100644 --- a/internal/domain/content/repository/repo.go +++ b/internal/domain/content/repository/repo.go @@ -5,6 +5,7 @@ type Repository interface { NewContent(ci any, data []byte) error AllContent(namespace string) [][]byte + ContentByPrefix(namespace, prefix string) ([][]byte, error) GetContent(namespace string, id string) ([]byte, error) DeleteContent(namespace string, id string, slug string) error diff --git a/internal/domain/content/valueobject/site.go b/internal/domain/content/valueobject/site.go index eb2f10ac..62661ac1 100644 --- a/internal/domain/content/valueobject/site.go +++ b/internal/domain/content/valueobject/site.go @@ -20,11 +20,11 @@ type Site struct { Theme string `json:"theme"` Params string `json:"params"` Owner string `json:"owner"` - WorkingDir string `json:"working_dir"` - GoogleAnalytics string `json:"google_analytics"` - DefaultContentLanguage string `json:"default_content_language"` - Languages []string `json:"languages"` - Menus []string `json:"menus"` + WorkingDir string `json:"working_dir,omitempty"` + GoogleAnalytics string `json:"google_analytics,omitempty"` + DefaultContentLanguage string `json:"default_content_language,omitempty"` + Languages []string `json:"languages,omitempty"` + Menus []string `json:"menus,omitempty"` } // MarshalEditor writes a buffer of html to edit a Song within the CMS @@ -217,8 +217,12 @@ description = "{{.Description}}" baseURL = "{{.BaseURL}}" owner = "{{.Owner}}" +{{ if .DefaultContentLanguage }} defaultContentLanguage = "{{.DefaultContentLanguage}}" +{{ end }} +{{ if .GoogleAnalytics }} googleAnalytics = "{{.GoogleAnalytics}}" +{{ end }} [module] [[module.imports]] diff --git a/internal/domain/contenthub/entity/contenthub.go b/internal/domain/contenthub/entity/contenthub.go index ece1ab3f..b43cbba5 100644 --- a/internal/domain/contenthub/entity/contenthub.go +++ b/internal/domain/contenthub/entity/contenthub.go @@ -110,9 +110,9 @@ func (ch *ContentHub) GetPageSources(page contenthub.Page) ([]contenthub.PageSou return v, nil } -func (ch *ContentHub) GlobalPages() contenthub.Pages { +func (ch *ContentHub) GlobalPages(langIndex int) contenthub.Pages { return ch.PageMap.getPagesInSection( - 0, + langIndex, pageMapQueryPagesInSection{ pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{ Path: "", diff --git a/internal/domain/contenthub/entity/pagefields.go b/internal/domain/contenthub/entity/pagefields.go index 4c528ee9..9b5e3c48 100644 --- a/internal/domain/contenthub/entity/pagefields.go +++ b/internal/domain/contenthub/entity/pagefields.go @@ -78,10 +78,19 @@ func (p *Page) RegularPages() contenthub.Pages { return nil } +func (p *Page) Sections(langIndex int) contenthub.Pages { + prefix := paths.AddTrailingSlash(p.Paths().Base()) + return p.pageMap.getSections(langIndex, prefix) +} + func (p *Page) Terms(langIndex int, taxonomy string) contenthub.Pages { return p.pageMap.getTermsForPageInTaxonomy(p.Paths().Base(), taxonomy) } +func (p *Page) IsTranslated() bool { + return len(p.Translations()) > 0 +} + func (p *Page) Translations() contenthub.Pages { key := p.Path() + "/" + p.PageLanguage() + "/" + "translations" pages, err := p.pageMap.getOrCreatePagesFromCache(nil, key, func(string) (contenthub.Pages, error) { diff --git a/internal/domain/contenthub/entity/pagemap.go b/internal/domain/contenthub/entity/pagemap.go index 3c22f5cd..b6ac2c98 100644 --- a/internal/domain/contenthub/entity/pagemap.go +++ b/internal/domain/contenthub/entity/pagemap.go @@ -427,3 +427,42 @@ func (m *PageMap) getTermsForPageInTaxonomy(base, taxonomy string) contenthub.Pa return v } + +func (m *PageMap) getSections(langIndex int, prefix string) contenthub.Pages { + var ( + pages contenthub.Pages + currentBranchPrefix string + tree = m.TreePages.Shape(0, langIndex) + ) + + w := &doctree.NodeShiftTreeWalker[*PageTreesNode]{ + Tree: tree, + Prefix: prefix, + } + w.Handle = func(ss string, n *PageTreesNode, match doctree.DimensionFlag) (bool, error) { + p, found := n.getPage() + if !found { + return false, nil + } + + if p.IsPage() { + return false, nil + } + if currentBranchPrefix == "" || !strings.HasPrefix(ss, currentBranchPrefix) { + if p.IsSection() && p.ShouldList(false) && p.Parent() == p { + pages = append(pages, p) + } else { + w.SkipPrefix(ss + "/") + } + } + currentBranchPrefix = ss + "/" + return false, nil + } + + if err := w.Walk(context.Background()); err != nil { + panic(err) + } + + valueobject.SortByDefault(pages) + return pages +} diff --git a/internal/domain/contenthub/type.go b/internal/domain/contenthub/type.go index 05e60cba..5acd7a50 100644 --- a/internal/domain/contenthub/type.go +++ b/internal/domain/contenthub/type.go @@ -273,8 +273,11 @@ type Page interface { Parent() Page Pages(langIndex int) Pages + Sections(langIndex int) Pages RegularPages() Pages Terms(langIndex int, taxonomy string) Pages + + IsTranslated() bool Translations() Pages } @@ -317,12 +320,18 @@ type PagerManager interface { Paginate(groups PageGroups) (Pager, error) } +type Pagers []Pager + type Pager interface { PageNumber() int TotalPages() int URL() string Pages() Pages + + Pagers() Pagers + First() Pager + Last() Pager HasPrev() bool Prev() Pager HasNext() bool diff --git a/internal/domain/contenthub/valueobject/pagenop.go b/internal/domain/contenthub/valueobject/pagenop.go index 1b9864b1..493427ec 100644 --- a/internal/domain/contenthub/valueobject/pagenop.go +++ b/internal/domain/contenthub/valueobject/pagenop.go @@ -15,6 +15,16 @@ var ( // PageNop implements Page, but does nothing. type nopPage int +func (p *nopPage) Sections(langIndex int) contenthub.Pages { + //TODO implement me + panic("implement me") +} + +func (p *nopPage) IsTranslated() bool { + //TODO implement me + panic("implement me") +} + func (p *nopPage) PageDate() time.Time { //TODO implement me panic("implement me") diff --git a/internal/domain/contenthub/valueobject/pager.go b/internal/domain/contenthub/valueobject/pager.go index 81785345..d2060d51 100644 --- a/internal/domain/contenthub/valueobject/pager.go +++ b/internal/domain/contenthub/valueobject/pager.go @@ -66,6 +66,14 @@ func (p *Pager) Next() contenthub.Pager { return p.pagers[p.PageNumber()] } +func (p *Pager) First() contenthub.Pager { + return p.pagers[0] +} + +func (p *Pager) Last() contenthub.Pager { + return p.pagers[len(p.pagers)-1] +} + func (p *Pager) URL() string { pageNumber := p.PageNumber() if pageNumber > 1 { diff --git a/internal/domain/contenthub/valueobject/paginator.go b/internal/domain/contenthub/valueobject/paginator.go index eacde985..ec006609 100644 --- a/internal/domain/contenthub/valueobject/paginator.go +++ b/internal/domain/contenthub/valueobject/paginator.go @@ -116,8 +116,14 @@ func splitPageGroups(pageGroups contenthub.PageGroups, size int) []paginatedElem return split } -func (p *Paginator) Pagers() pagers { - return p.pagers +func (p *Paginator) Pagers() contenthub.Pagers { + var ps contenthub.Pagers + + for _, pager := range p.pagers { + ps = append(ps, pager) + } + + return ps } func (p *Paginator) TotalPages() int { diff --git a/internal/domain/markdown/type.go b/internal/domain/markdown/type.go index 8783f0f1..5723afc2 100644 --- a/internal/domain/markdown/type.go +++ b/internal/domain/markdown/type.go @@ -199,9 +199,14 @@ type Header interface { Level() int Links() []Link + Paragraphs() []Paragraph } type Link interface { Text() string URL() string } + +type Paragraph interface { + Text() string +} diff --git a/internal/domain/markdown/valueobject/nodeheader.go b/internal/domain/markdown/valueobject/nodeheader.go index ea8bd5d9..24a07401 100644 --- a/internal/domain/markdown/valueobject/nodeheader.go +++ b/internal/domain/markdown/valueobject/nodeheader.go @@ -53,3 +53,24 @@ func (h *HeaderNode) Links() []markdown.Link { return links } + +func (h *HeaderNode) Paragraphs() []markdown.Paragraph { + var paragraphs []markdown.Paragraph + + for sibling := h.node.NextSibling(); sibling != nil; sibling = sibling.NextSibling() { + // 如果遇到下一个 Header,停止收集 + if heading, ok := sibling.(*ast.Heading); ok { + if heading.Level <= h.Level() { + break + } + } + + // 检查段落节点 + if paragraph, ok := sibling.(*ast.Paragraph); ok { + text := extractAllTextFromNode(paragraph, h.src) + paragraphs = append(paragraphs, &ParagraphNode{text: text}) + } + } + + return paragraphs +} diff --git a/internal/domain/markdown/valueobject/nodeparagraph.go b/internal/domain/markdown/valueobject/nodeparagraph.go new file mode 100644 index 00000000..17bb2558 --- /dev/null +++ b/internal/domain/markdown/valueobject/nodeparagraph.go @@ -0,0 +1,9 @@ +package valueobject + +type ParagraphNode struct { + text string +} + +func (p *ParagraphNode) Text() string { + return p.text +} diff --git a/internal/domain/markdown/valueobject/parserresult.go b/internal/domain/markdown/valueobject/parserresult.go index d72610a7..9cc852a2 100644 --- a/internal/domain/markdown/valueobject/parserresult.go +++ b/internal/domain/markdown/valueobject/parserresult.go @@ -3,6 +3,7 @@ package valueobject import ( "github.com/gohugonet/hugoverse/internal/domain/markdown" "github.com/yuin/goldmark/ast" + "strings" ) type ParserResult struct { @@ -61,3 +62,32 @@ func extractTextFromNode(node ast.Node, src []byte) string { } return text } + +func extractAllTextFromNode(node ast.Node, source []byte) string { + var textBuilder strings.Builder + + // 定义 Walker 函数 + walker := func(n ast.Node, entering bool) (ast.WalkStatus, error) { + if !entering { + return ast.WalkContinue, nil + } + + switch n := n.(type) { + case *ast.Text: + textBuilder.Write(n.Segment.Value(source)) // 提取普通文本 + + case *ast.AutoLink: + textBuilder.Write(n.Label(source)) // 提取自动链接(如邮箱、URL) + + case *ast.Link: + textBuilder.Write(n.Text(source)) // 提取显式链接 + } + + return ast.WalkContinue, nil + } + + // 使用 Walk 遍历整个节点树 + _ = ast.Walk(node, walker) + + return textBuilder.String() +} diff --git a/internal/domain/resources/entity/bundler.go b/internal/domain/resources/entity/bundler.go new file mode 100644 index 00000000..5ee9b1e5 --- /dev/null +++ b/internal/domain/resources/entity/bundler.go @@ -0,0 +1,144 @@ +package entity + +import ( + "fmt" + "github.com/gohugonet/hugoverse/internal/domain/resources" + "github.com/gohugonet/hugoverse/pkg/identity" + pio "github.com/gohugonet/hugoverse/pkg/io" + "github.com/gohugonet/hugoverse/pkg/media" + "io" + "path" +) + +type BundlerClient struct { + rs *Resources +} + +func NewBundlerClient(rs *Resources) *BundlerClient { + return &BundlerClient{ + rs: rs, + } +} + +// Concat concatenates the list of Resource objects. +func (c *BundlerClient) Concat(targetPath string, r []resources.Resource) (resources.Resource, error) { + targetPath = path.Clean(targetPath) + return c.rs.Cache.GetOrCreateResource(targetPath, func() (resources.Resource, error) { + var resolvedm media.Type + + // The given set of resources must be of the same Media Type. + // We may improve on that in the future, but then we need to know more. + for i, rr := range r { + if i > 0 && rr.MediaType().Type != resolvedm.Type { + return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", rr.MediaType().Type, resolvedm.Type) + } + resolvedm = rr.MediaType() + } + + idm := identity.NewManager("concat") + // Add the concatenated resources as dependencies to the composite resource + // so that we can track changes to the individual resources. + idm.AddIdentityForEach(identity.ForEeachIdentityProviderFunc( + func(f func(identity.Identity) bool) bool { + var terminate bool + for _, rr := range r { + identity.WalkIdentitiesShallow(rr, func(depth int, id identity.Identity) bool { + terminate = f(id) + return terminate + }) + if terminate { + break + } + } + return terminate + }, + )) + + concatr := func() (pio.ReadSeekCloser, error) { + var rcsources []pio.ReadSeekCloser + for _, s := range r { + rcr, ok := s.(resources.ReadSeekCloserResource) + if !ok { + return nil, fmt.Errorf("resource %T does not implement resource.ReadSeekerCloserResource", s) + } + rc, err := rcr.ReadSeekCloser() + if err != nil { + // Close the already opened. + for _, rcs := range rcsources { + rcs.Close() + } + return nil, err + } + + rcsources = append(rcsources, rc) + } + + // Arbitrary JavaScript files require a barrier between them to be safely concatenated together. + // Without this, the last line of one file can affect the first line of the next file and change how both files are interpreted. + if resolvedm.MainType == media.Builtin.JavascriptType.MainType && resolvedm.SubType == media.Builtin.JavascriptType.SubType { + readers := make([]pio.ReadSeekCloser, 2*len(rcsources)-1) + j := 0 + for i := 0; i < len(rcsources); i++ { + if i > 0 { + readers[j] = pio.NewReadSeekerNoOpCloserFromString("\n;\n") + j++ + } + readers[j] = rcsources[i] + j++ + } + return newMultiReadSeekCloser(readers...), nil + } + + return newMultiReadSeekCloser(rcsources...), nil + } + + rsb := newResourceBuilder(targetPath, concatr) + rsb.withCache(c.rs.Cache).withMediaService(c.rs.MediaService). + withImageService(c.rs.ImageService).withImageProcessor(c.rs.ImageProc). + withPublisher(c.rs.Publisher).withURLService(c.rs.URLService) + + return rsb.build() + }) +} + +func newMultiReadSeekCloser(sources ...pio.ReadSeekCloser) *multiReadSeekCloser { + mr := io.MultiReader(toReaders(sources)...) + return &multiReadSeekCloser{mr, sources} +} + +type multiReadSeekCloser struct { + mr io.Reader + sources []pio.ReadSeekCloser +} + +func toReaders(sources []pio.ReadSeekCloser) []io.Reader { + readers := make([]io.Reader, len(sources)) + for i, r := range sources { + readers[i] = r + } + return readers +} + +func (r *multiReadSeekCloser) Read(p []byte) (n int, err error) { + return r.mr.Read(p) +} + +func (r *multiReadSeekCloser) Seek(offset int64, whence int) (newOffset int64, err error) { + for _, s := range r.sources { + newOffset, err = s.Seek(offset, whence) + if err != nil { + return + } + } + + r.mr = io.MultiReader(toReaders(r.sources)...) + + return +} + +func (r *multiReadSeekCloser) Close() error { + for _, s := range r.sources { + s.Close() + } + return nil +} diff --git a/internal/domain/resources/entity/resources.go b/internal/domain/resources/entity/resources.go index 8efa1d19..2c268329 100644 --- a/internal/domain/resources/entity/resources.go +++ b/internal/domain/resources/entity/resources.go @@ -34,6 +34,7 @@ type Resources struct { *TemplateClient *IntegrityClient *SassClient + *BundlerClient } func (rs *Resources) SetupTemplateClient(tmpl Template) { diff --git a/internal/domain/resources/factory/resource.go b/internal/domain/resources/factory/resource.go index 35220aa8..ba52021e 100644 --- a/internal/domain/resources/factory/resource.go +++ b/internal/domain/resources/factory/resource.go @@ -69,6 +69,8 @@ func NewResources(ws resources.Workspace) (*entity.Resources, error) { SassClient: ds, } + rs.BundlerClient = entity.NewBundlerClient(rs) + return rs, nil } diff --git a/internal/domain/resources/type.go b/internal/domain/resources/type.go index 729591fc..30945bed 100644 --- a/internal/domain/resources/type.go +++ b/internal/domain/resources/type.go @@ -103,6 +103,7 @@ type Resource interface { Data() any ResourceType() string + MediaType() media.Type } type ResourceCopier interface { diff --git a/internal/domain/site/entity/hugo.go b/internal/domain/site/entity/hugo.go index a29b25dc..d1e103d2 100644 --- a/internal/domain/site/entity/hugo.go +++ b/internal/domain/site/entity/hugo.go @@ -27,3 +27,7 @@ func (s *Site) IsMultilingual() bool { func (s *Site) IsMultihost() bool { return false } + +func (s *Site) Copyright() string { + return "" +} diff --git a/internal/domain/site/entity/pagefields.go b/internal/domain/site/entity/pagefields.go index 7e7ef8e9..b673c20a 100644 --- a/internal/domain/site/entity/pagefields.go +++ b/internal/domain/site/entity/pagefields.go @@ -80,20 +80,6 @@ func (p *Page) Translations() []*Page { return p.sitePages(p.Page.Translations()) } -func (p *Page) sitePages(ps contenthub.Pages) []*Page { - var pages []*Page - for _, cp := range ps { - np, err := p.Site.sitePage(cp) - if err != nil { - continue - } - - pages = append(pages, np) - } - - return pages -} - func (p *Page) Parent() *Page { if p.IsHome() { return nil @@ -162,12 +148,15 @@ func (p *Page) Title() string { func (p *Page) Language() struct { Lang string LanguageName string + LanguageCode string } { return struct { Lang string LanguageName string + LanguageCode string }{ Lang: p.PageIdentity().PageLanguage(), LanguageName: p.Site.Language.LangSvc.GetLanguageName(p.PageIdentity().PageLanguage()), + LanguageCode: p.PageIdentity().PageLanguage(), } } diff --git a/internal/domain/site/entity/paginator.go b/internal/domain/site/entity/paginator.go index ef9a33fa..2fee3bc0 100644 --- a/internal/domain/site/entity/paginator.go +++ b/internal/domain/site/entity/paginator.go @@ -32,9 +32,15 @@ func (p *SitePager) Pages() Pages { } func (p *SitePager) Prev() *SitePager { + if !p.Pager.HasPrev() { + return nil + } return &SitePager{p.page, p.Pager.Prev()} } func (p *SitePager) Next() *SitePager { + if !p.Pager.HasNext() { + return nil + } return &SitePager{p.page, p.Pager.Next()} } diff --git a/internal/domain/site/entity/reserve.go b/internal/domain/site/entity/reserve.go new file mode 100644 index 00000000..22377dd4 --- /dev/null +++ b/internal/domain/site/entity/reserve.go @@ -0,0 +1,33 @@ +package entity + +import ( + "github.com/gohugonet/hugoverse/internal/domain/site/valueobject" + "github.com/gohugonet/hugoverse/pkg/maps" +) + +type Reserve struct { + site *Site +} + +func NewReserve(site *Site) *Reserve { + return &Reserve{ + site: site, + } +} + +func (r *Reserve) Contact() maps.Params { + params := maps.Params{} + lp, err := r.site.GetPage(valueobject.ReservedAboutContactFile) + + if lp != nil && err == nil { + hs := lp.Result().Headers() + for _, h := range hs { + paragraphs := h.Paragraphs() + if len(paragraphs) > 0 { + params[h.Name()] = paragraphs[0].Text() + } + } + } + + return params +} diff --git a/internal/domain/site/entity/site.go b/internal/domain/site/entity/site.go index 580ab52f..6cd51ed9 100644 --- a/internal/domain/site/entity/site.go +++ b/internal/domain/site/entity/site.go @@ -17,6 +17,7 @@ type Site struct { TranslationSvc site.TranslationService ResourcesSvc site.ResourceService LanguageSvc site.LanguageService + Sitemap site.SitemapService GitSvc *valueobject.GitMap @@ -33,6 +34,7 @@ type Site struct { *Ref *Language *Navigation + *Reserve home *Page diff --git a/internal/domain/site/entity/sitefields.go b/internal/domain/site/entity/sitefields.go index 1869236a..7ed3899c 100644 --- a/internal/domain/site/entity/sitefields.go +++ b/internal/domain/site/entity/sitefields.go @@ -10,13 +10,24 @@ import ( ) func (s *Site) Params() maps.Params { - return s.ConfigSvc.ConfigParams() + cp := s.ConfigSvc.ConfigParams() + ps := s.Reserve.Contact() + + maps.MergeParams(cp, ps) + + return cp } func (s *Site) Home() *Page { return s.home } +func (s *Site) Sections() []*Page { + pgs := s.home.Page.Sections(s.CurrentLanguageIndex()) + + return s.sitePages(pgs) +} + func (s *Site) IsMultiLingual() bool { return s.Language.isMultipleLanguage() } @@ -47,34 +58,15 @@ func (s *Site) GetPage(ref ...string) (*Page, error) { } func (s *Site) Pages() []*Page { - cps := s.ContentSvc.GlobalPages() - - var pages []*Page + cps := s.ContentSvc.GlobalPages(s.CurrentLanguageIndex()) - for _, cp := range cps { - p, err := s.sitePage(cp) - if err != nil { - continue - } - pages = append(pages, p) - } - return pages + return s.sitePages(cps) } func (s *Site) RegularPages() []*Page { cps := s.ContentSvc.GlobalRegularPages() - var pages []*Page - - for _, cp := range cps { - p, err := s.sitePage(cp) - if err != nil { - continue - } - pages = append(pages, p) - } - return pages - + return s.sitePages(cps) } func (s *Site) pageOutput(p contenthub.Page) (contenthub.PageOutput, error) { @@ -90,6 +82,20 @@ func (s *Site) pageOutput(p contenthub.Page) (contenthub.PageOutput, error) { return po, nil } +func (s *Site) sitePages(ps contenthub.Pages) []*Page { + var pages []*Page + for _, cp := range ps { + np, err := s.sitePage(cp) + if err != nil { + continue + } + + pages = append(pages, np) + } + + return pages +} + func (s *Site) sitePage(p contenthub.Page) (*Page, error) { po, err := s.pageOutput(p) if err != nil { diff --git a/internal/domain/site/entity/siteinit.go b/internal/domain/site/entity/siteinit.go index 551f72f7..9e9e8323 100644 --- a/internal/domain/site/entity/siteinit.go +++ b/internal/domain/site/entity/siteinit.go @@ -54,8 +54,8 @@ func (s *Site) PrepareLazyLoads() { for i, l := range h.Links() { menus[valueobject.MenusAfter] = menus[valueobject.MenusAfter].Add(&valueobject.MenuEntry{ MenuConfig: valueobject.MenuConfig{ - Name: l.Text(), // TODO, relative path - URL: l.URL(), + Name: l.Text(), + URL: s.AbsURL(l.URL()), Weight: valueobject.ReservedLinksWeight + i, }, Menu: valueobject.MenusAfter, diff --git a/internal/domain/site/factory/site.go b/internal/domain/site/factory/site.go index 0e308a43..a82092a3 100644 --- a/internal/domain/site/factory/site.go +++ b/internal/domain/site/factory/site.go @@ -23,6 +23,7 @@ func New(services site.Services) *entity.Site { TranslationSvc: services, ResourcesSvc: services, LanguageSvc: services, + Sitemap: services, GitSvc: git, @@ -54,6 +55,7 @@ func New(services site.Services) *entity.Site { s.PrepareLazyLoads() s.Ref.Site = s + s.Reserve = entity.NewReserve(s) return s } diff --git a/internal/domain/site/type.go b/internal/domain/site/type.go index 4f66dff1..ff88945e 100644 --- a/internal/domain/site/type.go +++ b/internal/domain/site/type.go @@ -22,6 +22,12 @@ type Services interface { FsService URLService ConfigService + SitemapService +} + +type SitemapService interface { + ChangeFreq() string + Priority() float64 } type ConfigService interface { @@ -47,7 +53,7 @@ type ContentService interface { WalkPages(langIndex int, walker contenthub.WalkFunc) error GetPageSources(page contenthub.Page) ([]contenthub.PageSource, error) WalkTaxonomies(langIndex int, walker contenthub.WalkTaxonomyFunc) error - GlobalPages() contenthub.Pages + GlobalPages(langIndex int) contenthub.Pages GlobalRegularPages() contenthub.Pages GetPageFromPath(langIndex int, path string) (contenthub.Page, error) diff --git a/internal/domain/site/valueobject/menu.go b/internal/domain/site/valueobject/menu.go index be7e63aa..39deb17d 100644 --- a/internal/domain/site/valueobject/menu.go +++ b/internal/domain/site/valueobject/menu.go @@ -26,6 +26,19 @@ type MenuEntry struct { Page *MenuEntry } +// HasChildren returns whether this menu item has any children. +func (m *MenuEntry) HasChildren() bool { + return m.Children != nil +} + +// KeyName returns the key used to identify this menu entry. +func (m *MenuEntry) KeyName() string { + if m.Identifier != "" { + return m.Identifier + } + return m.Name +} + // Menu is a collection of menu entries. type Menu []*MenuEntry diff --git a/internal/domain/site/valueobject/reserved.go b/internal/domain/site/valueobject/reserved.go index dd16a3e6..386745df 100644 --- a/internal/domain/site/valueobject/reserved.go +++ b/internal/domain/site/valueobject/reserved.go @@ -1,5 +1,6 @@ package valueobject const ReservedLinksWeight = 100 -const ReservedLinksFile = "/hv-links.md" +const ReservedLinksFile = "/links.md" const ReservedLinksMenuSection = "menu" +const ReservedAboutContactFile = "/about/contact.md" diff --git a/internal/domain/template/entity/embedded/templates/_default/sitemap.xml b/internal/domain/template/entity/embedded/templates/_default/sitemap.xml index b1e4b2d2..de2920cb 100644 --- a/internal/domain/template/entity/embedded/templates/_default/sitemap.xml +++ b/internal/domain/template/entity/embedded/templates/_default/sitemap.xml @@ -1,7 +1,7 @@ {{ printf "" | safeHTML }} - {{ range .Data.Pages }} + {{ range .Site.Pages }} {{- if .Permalink -}} {{ .Permalink }}{{ if not .Lastmod.IsZero }} diff --git a/internal/interfaces/api/database/db.go b/internal/interfaces/api/database/db.go index 305a8347..87b61177 100644 --- a/internal/interfaces/api/database/db.go +++ b/internal/interfaces/api/database/db.go @@ -86,6 +86,10 @@ func (d *Database) AllContent(namespace string) [][]byte { return d.getStore(namespace).ContentAll(namespace) } +func (d *Database) ContentByPrefix(namespace, prefix string) ([][]byte, error) { + return d.getStore(namespace).ContentByPrefix(bucketNameWithIndex(namespace), prefix) +} + func (d *Database) GetContent(namespace string, id string) ([]byte, error) { return d.getStore(namespace).Get( &item{ @@ -99,6 +103,10 @@ func (d *Database) DeleteContent(namespace string, id string, slug string) error return err } + if err := d.getStore(namespace).Delete(&item{bucket: bucketNameWithIndex(namespace), key: slug}); err != nil { + return err + } + if err := d.getStore(namespace).RemoveIndex(slug); err != nil { return err } @@ -134,6 +142,18 @@ func (d *Database) PutContent(ci any, data []byte) error { return err } + ciSlug, ok := ci.(content.Sluggable) + if ok { + if err := d.getStore(ns).Set( + &item{ + bucket: bucketNameWithIndex(ns), + key: ciSlug.ItemSlug(), + value: data, + }); err != nil { + return err + } + } + return nil } diff --git a/internal/interfaces/api/database/dbitem.go b/internal/interfaces/api/database/dbitem.go index e99a9062..bcc1d752 100644 --- a/internal/interfaces/api/database/dbitem.go +++ b/internal/interfaces/api/database/dbitem.go @@ -6,11 +6,16 @@ import ( ) const ItemBucketPrefix = "__" +const ItemBucketIndexSuffix = "__index" func bucketNameWithPrefix(name string) string { return ItemBucketPrefix + name } +func bucketNameWithIndex(name string) string { + return name + ItemBucketIndexSuffix +} + func newUploadItem(id string, data []byte) *item { return &item{ bucket: bucketNameWithPrefix("uploads"), diff --git a/internal/interfaces/cli/vercurr.go b/internal/interfaces/cli/vercurr.go index 4e63ca32..99ff5459 100644 --- a/internal/interfaces/cli/vercurr.go +++ b/internal/interfaces/cli/vercurr.go @@ -3,6 +3,6 @@ package cli var CurrentVersion = Version{ Major: 0, Minor: 1, - PatchLevel: 5, + PatchLevel: 6, Suffix: "", } diff --git a/manifest.json b/manifest.json index a2469507..7f899ffc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { - "version": "0.1.5", + "version": "0.1.6", "name": "Hugoverse", "description": "Headless CMS for Hugo", "author": "sunwei", diff --git a/pkg/db/content.go b/pkg/db/content.go index 435df62e..9fc337da 100644 --- a/pkg/db/content.go +++ b/pkg/db/content.go @@ -1,6 +1,7 @@ package db import ( + "bytes" "fmt" bolt "go.etcd.io/bbolt" "log" @@ -36,3 +37,32 @@ func (s *Store) ContentAll(namespace string) [][]byte { return posts } + +// ContentByPrefix retrieves all raw byte items from the database with a specific slug prefix. +func (s *Store) ContentByPrefix(namespace, prefix string) ([][]byte, error) { + var results [][]byte + + err := s.db.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte(namespace)) + if b == nil { + fmt.Println("Bucket not found:", namespace) + return bolt.ErrBucketNotFound + } + + c := b.Cursor() + + // 定位到第一个匹配 slugPrefix 的键 + for k, v := c.Seek([]byte(prefix)); k != nil && bytes.HasPrefix(k, []byte(prefix)); k, v = c.Next() { + results = append(results, v) + } + + return nil + }) + + if err != nil { + fmt.Println("Error reading from db with slug prefix:", namespace, err) + return nil, err + } + + return results, nil +} diff --git a/pkg/db/store.go b/pkg/db/store.go index 6c171f32..7439c080 100644 --- a/pkg/db/store.go +++ b/pkg/db/store.go @@ -38,6 +38,11 @@ func NewStore(dataDir string, contentTypes []string) (*Store, error) { if err != nil { return err } + + _, err = tx.CreateBucketIfNotExists([]byte(t + "__index")) + if err != nil { + return err + } } return nil diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index e5a921c2..3b7039ae 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -52,6 +52,13 @@ type ForEeachIdentityProvider interface { ForEeachIdentity(cb func(id Identity) bool) bool } +// ForEeachIdentityProviderFunc is a function that implements the ForEeachIdentityProvider interface. +type ForEeachIdentityProviderFunc func(func(id Identity) bool) bool + +func (f ForEeachIdentityProviderFunc) ForEeachIdentity(cb func(id Identity) bool) bool { + return f(cb) +} + // WalkIdentitiesShallow will not walk into a Manager's Identities. // See WalkIdentitiesDeep. // cb is called for every Identity found and returns whether to terminate the walk. diff --git a/pkg/maps/params.go b/pkg/maps/params.go index a8cbba55..b15d2b01 100644 --- a/pkg/maps/params.go +++ b/pkg/maps/params.go @@ -97,8 +97,9 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) { ms = ps } - noUpdate := ms == ParamsMergeStrategyNone - noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow) + if ms == ParamsMergeStrategyNone { + return + } for k, v := range pp { @@ -114,8 +115,10 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) { vvv.merge(ms, pv) } } - } else if !noUpdate { - p[k] = v + } else { + if ps == ParamsMergeStrategyShallow { + p[k] = v + } } } diff --git a/pkg/template/funcs/resource/resources.go b/pkg/template/funcs/resource/resources.go index da6449cb..269e5426 100644 --- a/pkg/template/funcs/resource/resources.go +++ b/pkg/template/funcs/resource/resources.go @@ -9,6 +9,7 @@ import ( "github.com/gohugonet/hugoverse/pkg/maps" "github.com/gohugonet/hugoverse/pkg/template/funcs/resource/resourcehelpers" "github.com/spf13/cast" + "reflect" ) // New returns a new instance of the resources-namespaced template functions. @@ -182,3 +183,28 @@ func (ns *Namespace) ToCSS(args ...any) (resources.Resource, error) { return ns.resourceService.ToCSS(r, m) } + +// Concat concatenates a slice of Resource objects. These resources must +// (currently) be of the same Media Type. +func (ns *Namespace) Concat(targetPathIn any, r any) (resources.Resource, error) { + targetPath, err := cast.ToStringE(targetPathIn) + if err != nil { + return nil, err + } + + rv := reflect.ValueOf(r) + if rv.Kind() != reflect.Slice { + return nil, errors.New("expected slice of Resource objects, received " + rv.Kind().String() + " instead") + } + + var rr []resources.Resource + for i := 0; i < rv.Len(); i++ { + rr = append(rr, rv.Index(i).Interface().(resources.Resource)) + } + + if len(rr) == 0 { + return nil, errors.New("must provide one or more Resource objects to concat") + } + + return ns.resourceService.Concat(targetPath, rr) +} diff --git a/pkg/template/funcs/resource/type.go b/pkg/template/funcs/resource/type.go index 41df4465..a60c3edb 100644 --- a/pkg/template/funcs/resource/type.go +++ b/pkg/template/funcs/resource/type.go @@ -17,4 +17,6 @@ type Resource interface { Fingerprint(res resources.Resource, algo string) (resources.Resource, error) ToCSS(res resources.Resource, args map[string]any) (resources.Resource, error) + + Concat(targetPath string, r []resources.Resource) (resources.Resource, error) }