diff --git a/go.mod b/go.mod index eb944753..099c8627 100644 --- a/go.mod +++ b/go.mod @@ -98,6 +98,7 @@ require ( github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.6.0 // indirect diff --git a/go.sum b/go.sum index 5d820213..77562eeb 100644 --- a/go.sum +++ b/go.sum @@ -355,6 +355,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= +github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ= github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= diff --git a/internal/domain/contenthub/entity/contenthub.go b/internal/domain/contenthub/entity/contenthub.go index 26804b1b..ece1ab3f 100644 --- a/internal/domain/contenthub/entity/contenthub.go +++ b/internal/domain/contenthub/entity/contenthub.go @@ -17,6 +17,7 @@ type ContentHub struct { TemplateExecutor contenthub.Template *Cache + *Translator *PageMap *PageFinder diff --git a/internal/domain/contenthub/entity/pagebuilder.go b/internal/domain/contenthub/entity/pagebuilder.go index 2471bf2e..51847f7b 100644 --- a/internal/domain/contenthub/entity/pagebuilder.go +++ b/internal/domain/contenthub/entity/pagebuilder.go @@ -199,6 +199,7 @@ func (b *PageBuilder) applyFrontMatter(p *Page) error { p.title = b.fm.Title p.Meta.Weight = b.fm.Weight p.Meta.Parameters = b.fm.Params + p.Meta.Date = b.fm.Date return nil } diff --git a/internal/domain/contenthub/entity/pagecontentprovider.go b/internal/domain/contenthub/entity/pagecontentprovider.go index 45de92ac..0f6a3642 100644 --- a/internal/domain/contenthub/entity/pagecontentprovider.go +++ b/internal/domain/contenthub/entity/pagecontentprovider.go @@ -151,8 +151,11 @@ func (c *ContentProvider) ContentSummary() (valueobject.ContentSummary, error) { } } - v.SummaryTruncated = c.content.summaryTruncated v.Content = helpers.BytesToHTML(b) + if v.IsSummaryEmpty() { + v.ExtractSummary(b, c.f.MediaType) + c.content.summaryTruncated = v.SummaryTruncated + } return &stale.Value[valueobject.ContentSummary]{ Value: v, diff --git a/internal/domain/contenthub/entity/pagemeta.go b/internal/domain/contenthub/entity/pagemeta.go index 877aba49..eb41c08b 100644 --- a/internal/domain/contenthub/entity/pagemeta.go +++ b/internal/domain/contenthub/entity/pagemeta.go @@ -1,6 +1,9 @@ package entity -import "github.com/gohugonet/hugoverse/pkg/maps" +import ( + "github.com/gohugonet/hugoverse/pkg/maps" + "time" +) const ( Never = "never" @@ -13,6 +16,8 @@ type Meta struct { List string Parameters maps.Params Weight int + + Date time.Time } func (m *Meta) Description() string { @@ -27,6 +32,10 @@ func (m *Meta) PageWeight() int { return m.Weight } +func (m *Meta) PageDate() time.Time { + return m.Date +} + func (m *Meta) ShouldList(global bool) bool { return m.shouldList(global) } diff --git a/internal/domain/contenthub/entity/translator.go b/internal/domain/contenthub/entity/translator.go new file mode 100644 index 00000000..e5a4ae92 --- /dev/null +++ b/internal/domain/contenthub/entity/translator.go @@ -0,0 +1,112 @@ +package entity + +import ( + "context" + "errors" + "fmt" + "github.com/gohugoio/go-i18n/v2/i18n" + "github.com/gohugonet/hugoverse/internal/domain/contenthub" + "github.com/gohugonet/hugoverse/internal/domain/contenthub/valueobject" + "github.com/gohugonet/hugoverse/pkg/hreflect" + "github.com/gohugonet/hugoverse/pkg/loggers" + "github.com/spf13/cast" + "reflect" + "strings" +) + +const ArtificialLangTagPrefix = "art-x-" + +type TranslateFunc func(ctx context.Context, translationID string, templateData any) string + +type Translator struct { + ContentLanguage string + TranslateFuncs map[string]TranslateFunc + + Log loggers.Logger `json:"-"` +} + +func (t *Translator) Translate(ctx context.Context, lang string, translationID string, templateData any) string { + if f, ok := t.TranslateFuncs[lang]; ok { + return f(ctx, translationID, templateData) + } + + t.Log.Infof("Translation func for language %v not found, use default.", lang) + if f, ok := t.TranslateFuncs[t.ContentLanguage]; ok { + return f(ctx, translationID, templateData) + } + + t.Log.Infoln("i18n not initialized; if you need string translations, check that you have a bundle in /i18n that matches the site language or the default language.") + + return "" +} + +func (t *Translator) SetupTranslateFuncs(bndl *i18n.Bundle) { + enableMissingTranslationPlaceholders := true + + for _, lang := range bndl.LanguageTags() { + currentLang := lang + currentLangStr := currentLang.String() + // This may be pt-BR; make it case insensitive. + currentLangKey := strings.ToLower(strings.TrimPrefix(currentLangStr, ArtificialLangTagPrefix)) + localizer := i18n.NewLocalizer(bndl, currentLangStr) + t.TranslateFuncs[currentLangKey] = func(ctx context.Context, translationID string, templateData any) string { + pluralCount := valueobject.GetPluralCount(templateData) + + if templateData != nil { + tp := reflect.TypeOf(templateData) + if hreflect.IsInt(tp.Kind()) { + // This was how go-i18n worked in v1, + // and we keep it like this to avoid breaking + // lots of sites in the wild. + templateData = valueobject.IntCount(cast.ToInt(templateData)) + } else { + //TODO setup with context + if _, ok := templateData.(contenthub.Page); ok { + // See issue 10782. + // The i18n has its own template handling and does not know about + // the context.Context. + // A common pattern is to pass Page to i18n, and use .ReadingTime etc. + // We need to improve this, but that requires some upstream changes. + // For now, just create a wrapper. + //templateData = page.PageWithContext{Page: p, Ctx: ctx} + } + } + } + + translated, translatedLang, err := localizer.LocalizeWithTag(&i18n.LocalizeConfig{ + MessageID: translationID, + TemplateData: templateData, + PluralCount: pluralCount, + }) + + sameLang := currentLang == translatedLang + + if err == nil && sameLang { + return translated + } + + if err != nil && sameLang && translated != "" { + // See #8492 + // TODO(bep) this needs to be improved/fixed upstream, + // but currently we get an error even if the fallback to + // "other" succeeds. + if fmt.Sprintf("%T", err) == "i18n.pluralFormNotFoundError" { + return translated + } + } + + var messageNotFoundErr *i18n.MessageNotFoundErr + if !errors.As(err, &messageNotFoundErr) { + t.Log.Warnf("Failed to get translated string for language %q and ID %q: %s", currentLangStr, translationID, err) + } + + t.Log.Warnf("i18n|MISSING_TRANSLATION|%s|%s", currentLangStr, translationID) + + if enableMissingTranslationPlaceholders { + return "[i18n] " + translationID + } + + return translated + } + } +} diff --git a/internal/domain/contenthub/factory/hub.go b/internal/domain/contenthub/factory/hub.go index 4167a2d1..a79d8fb9 100644 --- a/internal/domain/contenthub/factory/hub.go +++ b/internal/domain/contenthub/factory/hub.go @@ -1,29 +1,49 @@ package factory import ( + "fmt" "github.com/gohugonet/hugoverse/internal/domain/contenthub" "github.com/gohugonet/hugoverse/internal/domain/contenthub/entity" "github.com/gohugonet/hugoverse/internal/domain/contenthub/valueobject" + "github.com/gohugonet/hugoverse/internal/domain/fs" "github.com/gohugonet/hugoverse/pkg/cache/dynacache" "github.com/gohugonet/hugoverse/pkg/cache/stale" "github.com/gohugonet/hugoverse/pkg/doctree" + "github.com/gohugonet/hugoverse/pkg/helpers" + "github.com/gohugonet/hugoverse/pkg/herrors" "github.com/gohugonet/hugoverse/pkg/loggers" + "github.com/gohugonet/hugoverse/pkg/paths" + "golang.org/x/text/language" + "strings" + + "encoding/json" + "github.com/gohugoio/go-i18n/v2/i18n" + toml "github.com/pelletier/go-toml/v2" + yaml "gopkg.in/yaml.v2" ) func New(services contenthub.Services) (*entity.ContentHub, error) { log := loggers.NewDefault() + valueobject.SetupDefaultContentTypes() cs, err := newContentSpec() if err != nil { return nil, err } + t, err := newTranslator(services, log) + if err != nil { + return nil, err + } + cache := newCache() ch := &entity.ContentHub{ Cache: cache, Fs: services, TemplateExecutor: nil, + Translator: t, + PageMap: &entity.PageMap{ PageTrees: newPageTree(), @@ -181,3 +201,93 @@ func newConverterRegistry() (contenthub.ConverterRegistry, error) { Converters: converters, }, nil } + +func newTranslator(services contenthub.Services, log loggers.Logger) (*entity.Translator, error) { + defaultLangTag, err := language.Parse(services.DefaultLanguage()) + if err != nil { + defaultLangTag = language.English + } + bundle := i18n.NewBundle(defaultLangTag) + + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal) + bundle.RegisterUnmarshalFunc("yml", yaml.Unmarshal) + bundle.RegisterUnmarshalFunc("json", json.Unmarshal) + + if err := services.WalkI18n("", fs.WalkCallback{ + HookPre: nil, + WalkFn: func(path string, info fs.FileMetaInfo) error { + if info.IsDir() { + return nil + } + file, err := valueobject.NewFileInfo(info) + if err != nil { + return err + } + return addTranslationFile(bundle, file) + }, + HookPost: nil, + }, fs.WalkwayConfig{}); err != nil { + if !herrors.IsNotExist(err) { + return nil, err + } + } + + t := &entity.Translator{ + ContentLanguage: services.DefaultLanguage(), + TranslateFuncs: make(map[string]entity.TranslateFunc), + + Log: log, + } + t.SetupTranslateFuncs(bundle) + + return t, err +} + +func addTranslationFile(bundle *i18n.Bundle, r *valueobject.File) error { + f, err := r.Open() + if err != nil { + return fmt.Errorf("failed to open translations file %q:: %w", r.LogicalName(), err) + } + + b := helpers.ReaderToBytes(f) + f.Close() + + name := r.LogicalName() + lang := paths.Filename(name) + tag := language.Make(lang) + if tag == language.Und { + try := entity.ArtificialLangTagPrefix + lang + _, err = language.Parse(try) + if err != nil { + return fmt.Errorf("%q: %s", try, err) + } + name = entity.ArtificialLangTagPrefix + name + } + + _, err = bundle.ParseMessageFileBytes(b, name) + if err != nil { + if strings.Contains(err.Error(), "no plural rule") { + // https://github.com/gohugoio/hugo/issues/7798 + name = entity.ArtificialLangTagPrefix + name + _, err = bundle.ParseMessageFileBytes(b, name) + if err == nil { + return nil + } + } + return errWithFileContext(fmt.Errorf("failed to load translations: %w", err), r) + } + + return nil +} + +func errWithFileContext(inerr error, r *valueobject.File) error { + realFilename := r.Filename() + f, err := r.Open() + if err != nil { + return inerr + } + defer f.Close() + + return herrors.NewFileErrorFromName(inerr, realFilename).UpdateContent(f, nil) +} diff --git a/internal/domain/contenthub/type.go b/internal/domain/contenthub/type.go index d612dd31..39442d16 100644 --- a/internal/domain/contenthub/type.go +++ b/internal/domain/contenthub/type.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/afero" goTmpl "html/template" "io" + "time" ) type ContentHub interface { @@ -72,6 +73,7 @@ type FsService interface { ContentFs() afero.Fs WalkContent(start string, cb fs.WalkCallback, conf fs.WalkwayConfig) error + WalkI18n(start string, cb fs.WalkCallback, conf fs.WalkwayConfig) error ReverseLookupContent(filename string, checkExists bool) ([]fs.ComponentPath, error) } @@ -285,6 +287,7 @@ type PageMeta interface { Description() string Params() maps.Params PageWeight() int + PageDate() time.Time ShouldList(global bool) bool ShouldListAny() bool diff --git a/internal/domain/contenthub/valueobject/content.go b/internal/domain/contenthub/valueobject/content.go index be22c363..66308506 100644 --- a/internal/domain/contenthub/valueobject/content.go +++ b/internal/domain/contenthub/valueobject/content.go @@ -1,9 +1,38 @@ package valueobject import ( + "bytes" + "github.com/gohugonet/hugoverse/pkg/helpers" + "github.com/gohugonet/hugoverse/pkg/media" + "github.com/gohugonet/hugoverse/pkg/types" "html/template" + "regexp" + "strings" + "unicode" + "unicode/utf8" ) +// DefaultTocConfig is the default ToC configuration. +var DefaultTocConfig = TocConfig{ + StartLevel: 2, + EndLevel: 3, + Ordered: false, +} + +type TocConfig struct { + // Heading start level to include in the table of contents, starting + // at h1 (inclusive). + // { "identifiers": ["h1"] } + StartLevel int + + // Heading end level, inclusive, to include in the table of contents. + // Default is 3, a value of -1 will include everything. + EndLevel int + + // Whether to produce a ordered list or not. + Ordered bool +} + type ContentSummary struct { Content template.HTML Summary template.HTML @@ -20,23 +49,135 @@ func NewEmptyContentSummary() ContentSummary { } } -// DefaultTocConfig is the default ToC configuration. -var DefaultTocConfig = TocConfig{ - StartLevel: 2, - EndLevel: 3, - Ordered: false, +func (c *ContentSummary) IsSummaryEmpty() bool { + return c.Summary == "" } -type TocConfig struct { - // Heading start level to include in the table of contents, starting - // at h1 (inclusive). - // { "identifiers": ["h1"] } - StartLevel int +func (c *ContentSummary) ExtractSummary(input []byte, mt media.Type) { + in := string(input) - // Heading end level, inclusive, to include in the table of contents. - // Default is 3, a value of -1 will include everything. - EndLevel int + res := c.extractSummaryFromHTML(mt, in, 70, containsCJK(in)) + sum := res.Summary() + if sum != "" { + c.Summary = helpers.BytesToHTML([]byte(sum)) + c.SummaryTruncated = res.Truncated() + return + } - // Whether to produce a ordered list or not. - Ordered bool + ts := c.trimShortHTML(input) + c.Summary = helpers.BytesToHTML(input) + c.SummaryTruncated = len(ts) < len(in) + + return +} + +func (c *ContentSummary) extractSummaryFromHTML(mt media.Type, input string, numWords int, isCJK bool) (result *HtmlSummary) { + result = &HtmlSummary{source: input} + ptag := result.resolveParagraphTagAndSetWrapper(mt) + + if numWords <= 0 { + return result + } + + var count int + + countWord := func(word string) int { + word = strings.TrimSpace(word) + if len(word) == 0 { + return 0 + } + if isProbablyHTMLToken(word) { + return 0 + } + + if isCJK { + word = helpers.StripHTML(word) + runeCount := utf8.RuneCountInString(word) + if len(word) == runeCount { + return 1 + } else { + return runeCount + } + } + + return 1 + } + + high := len(input) + if result.WrapperEnd.Low > 0 { + high = result.WrapperEnd.Low + } + + for j := result.WrapperStart.High; j < high; { + s := input[j:] + closingIndex := strings.Index(s, "") + + if closingIndex == -1 { + break + } + + s = s[:closingIndex] + + // Count the words in the current paragraph. + var wi int + + for i, r := range s { + if unicode.IsSpace(r) || (i+utf8.RuneLen(r) == len(s)) { + word := s[wi:i] + count += countWord(word) + wi = i + if count >= numWords { + break + } + } + } + + if count >= numWords { + result.SummaryLowHigh = types.LowHigh[string]{ + Low: result.WrapperStart.High, + High: j + closingIndex + len(ptag.tagName) + 3, + } + return + } + + j += closingIndex + len(ptag.tagName) + 2 + + } + + result.SummaryLowHigh = types.LowHigh[string]{ + Low: result.WrapperStart.High, + High: high, + } + + return +} + +func containsCJK(s string) bool { + re := regexp.MustCompile(`[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]`) + return re.MatchString(s) +} + +// Avoid counting words that are most likely HTML tokens. +var ( + isProbablyHTMLTag = regexp.MustCompile(`^<\/?[A-Za-z]+>?$`) + isProablyHTMLAttribute = regexp.MustCompile(`^[A-Za-z]+=["']`) +) + +func isProbablyHTMLToken(s string) bool { + return s == ">" || isProbablyHTMLTag.MatchString(s) || isProablyHTMLAttribute.MatchString(s) +} + +func (c *ContentSummary) trimShortHTML(input []byte) []byte { + openingTag := []byte("

") + closingTag := []byte("

") + + if bytes.Count(input, openingTag) == 1 { + input = bytes.TrimSpace(input) + if bytes.HasPrefix(input, openingTag) && bytes.HasSuffix(input, closingTag) { + input = bytes.TrimPrefix(input, openingTag) + input = bytes.TrimSuffix(input, closingTag) + input = bytes.TrimSpace(input) + } + } + return input } diff --git a/internal/domain/contenthub/valueobject/contenttypes.go b/internal/domain/contenthub/valueobject/contenttypes.go new file mode 100644 index 00000000..b4bba5c7 --- /dev/null +++ b/internal/domain/contenthub/valueobject/contenttypes.go @@ -0,0 +1,107 @@ +package valueobject + +import ( + "github.com/gohugonet/hugoverse/pkg/media" + "path/filepath" + "strings" +) + +var DefaultContentTypes ContentTypes + +func SetupDefaultContentTypes() { + DefaultContentTypes = ContentTypes{ + HTML: media.Builtin.HTMLType, + Markdown: media.Builtin.MarkdownType, + AsciiDoc: media.Builtin.AsciiDocType, + Pandoc: media.Builtin.PandocType, + ReStructuredText: media.Builtin.ReStructuredTextType, + EmacsOrgMode: media.Builtin.EmacsOrgModeType, + } + + DefaultContentTypes.setup() +} + +// ContentTypes holds the media types that are considered content in Hugo. +type ContentTypes struct { + HTML media.Type + Markdown media.Type + AsciiDoc media.Type + Pandoc media.Type + ReStructuredText media.Type + EmacsOrgMode media.Type + + // Created in init(). + types media.Types + extensionSet map[string]bool +} + +func (t *ContentTypes) setup() { + t.types = media.Types{t.HTML, t.Markdown, t.AsciiDoc, t.Pandoc, t.ReStructuredText, t.EmacsOrgMode} + t.extensionSet = make(map[string]bool) + for _, mt := range t.types { + for _, suffix := range mt.Suffixes() { + t.extensionSet[suffix] = true + } + } +} + +func (t ContentTypes) IsContentSuffix(suffix string) bool { + return t.extensionSet[suffix] +} + +// IsContentFile returns whether the given filename is a content file. +func (t ContentTypes) IsContentFile(filename string) bool { + return t.IsContentSuffix(strings.TrimPrefix(filepath.Ext(filename), ".")) +} + +// IsIndexContentFile returns whether the given filename is an index content file. +func (t ContentTypes) IsIndexContentFile(filename string) bool { + if !t.IsContentFile(filename) { + return false + } + + base := filepath.Base(filename) + + return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.") +} + +// IsHTMLSuffix returns whether the given suffix is a HTML media type. +func (t ContentTypes) IsHTMLSuffix(suffix string) bool { + for _, s := range t.HTML.Suffixes() { + if s == suffix { + return true + } + } + return false +} + +// Types is a slice of media types. +func (t ContentTypes) Types() media.Types { + return t.types +} + +// FromTypes creates a new ContentTypes updated with the values from the given Types. +func (t ContentTypes) FromTypes(types media.Types) ContentTypes { + if tt, ok := types.GetByType(t.HTML.Type); ok { + t.HTML = tt + } + if tt, ok := types.GetByType(t.Markdown.Type); ok { + t.Markdown = tt + } + if tt, ok := types.GetByType(t.AsciiDoc.Type); ok { + t.AsciiDoc = tt + } + if tt, ok := types.GetByType(t.Pandoc.Type); ok { + t.Pandoc = tt + } + if tt, ok := types.GetByType(t.ReStructuredText.Type); ok { + t.ReStructuredText = tt + } + if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok { + t.EmacsOrgMode = tt + } + + t.setup() + + return t +} diff --git a/internal/domain/contenthub/valueobject/frontmatter.go b/internal/domain/contenthub/valueobject/frontmatter.go index 5fde3dd8..96357727 100644 --- a/internal/domain/contenthub/valueobject/frontmatter.go +++ b/internal/domain/contenthub/valueobject/frontmatter.go @@ -21,6 +21,8 @@ type FrontMatter struct { Title string Weight int + Date time.Time + Terms map[string][]string Params maps.Params @@ -65,11 +67,24 @@ func (b *FrontMatterParser) Parse() (*FrontMatter, error) { return nil, err } + if err := b.parseDate(fm); err != nil { + return nil, err + } + return fm, nil } +func (b *FrontMatterParser) parseDate(fm *FrontMatter) error { + fm.Date = time.Now() + if v, found := b.Params["date"]; found { + fm.Date = cast.ToTime(v) + } + + return nil +} + func (b *FrontMatterParser) parseWeight(fm *FrontMatter) error { - fm.Weight = 0 + fm.Weight = 10000 if v, found := b.Params["weight"]; found { fm.Weight = cast.ToInt(v) } diff --git a/internal/domain/contenthub/valueobject/i18n.go b/internal/domain/contenthub/valueobject/i18n.go new file mode 100644 index 00000000..4f92cd43 --- /dev/null +++ b/internal/domain/contenthub/valueobject/i18n.go @@ -0,0 +1,78 @@ +package valueobject + +import ( + "github.com/gohugonet/hugoverse/pkg/hreflect" + "github.com/spf13/cast" + "reflect" + "strings" +) + +// IntCount wraps the Count method. +type IntCount int + +func (c IntCount) Count() int { + return int(c) +} + +func GetPluralCount(v any) any { + if v == nil { + // i18n called without any argument, make sure it does not + // get any plural count. + return nil + } + + switch v := v.(type) { + case map[string]any: + for k, vv := range v { + if strings.EqualFold(k, countFieldName) { + return toPluralCountValue(vv) + } + } + default: + vv := reflect.Indirect(reflect.ValueOf(v)) + if vv.Kind() == reflect.Interface && !vv.IsNil() { + vv = vv.Elem() + } + tp := vv.Type() + + if tp.Kind() == reflect.Struct { + f := vv.FieldByName(countFieldName) + if f.IsValid() { + return toPluralCountValue(f.Interface()) + } + m := hreflect.GetMethodByName(vv, countFieldName) + if m.IsValid() && m.Type().NumIn() == 0 && m.Type().NumOut() == 1 { + c := m.Call(nil) + return toPluralCountValue(c[0].Interface()) + } + } + } + + return toPluralCountValue(v) +} + +const countFieldName = "Count" + +// go-i18n expects floats to be represented by string. +func toPluralCountValue(in any) any { + k := reflect.TypeOf(in).Kind() + switch { + case hreflect.IsFloat(k): + f := cast.ToString(in) + if !strings.Contains(f, ".") { + f += ".0" + } + return f + case k == reflect.String: + if _, err := cast.ToFloat64E(in); err == nil { + return in + } + // A non-numeric value. + return nil + default: + if i, err := cast.ToIntE(in); err == nil { + return i + } + return nil + } +} diff --git a/internal/domain/contenthub/valueobject/pagenop.go b/internal/domain/contenthub/valueobject/pagenop.go index a175b580..1b9864b1 100644 --- a/internal/domain/contenthub/valueobject/pagenop.go +++ b/internal/domain/contenthub/valueobject/pagenop.go @@ -5,6 +5,7 @@ import ( pio "github.com/gohugonet/hugoverse/pkg/io" "github.com/gohugonet/hugoverse/pkg/maps" "github.com/gohugonet/hugoverse/pkg/paths" + "time" ) var ( @@ -14,6 +15,11 @@ var ( // PageNop implements Page, but does nothing. type nopPage int +func (p *nopPage) PageDate() time.Time { + //TODO implement me + panic("implement me") +} + func (p *nopPage) Truncated() bool { //TODO implement me panic("implement me") diff --git a/internal/domain/contenthub/valueobject/summary.go b/internal/domain/contenthub/valueobject/summary.go new file mode 100644 index 00000000..4024687d --- /dev/null +++ b/internal/domain/contenthub/valueobject/summary.go @@ -0,0 +1,121 @@ +package valueobject + +import ( + "github.com/gohugonet/hugoverse/pkg/media" + "github.com/gohugonet/hugoverse/pkg/types" + "regexp" + "strings" +) + +type HtmlSummary struct { + source string + SummaryLowHigh types.LowHigh[string] + SummaryEndTag types.LowHigh[string] + WrapperStart types.LowHigh[string] + WrapperEnd types.LowHigh[string] + Divider types.LowHigh[string] +} + +func (s *HtmlSummary) wrap(ss string) string { + if s.WrapperStart.IsZero() { + return ss + } + return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss + s.source[s.WrapperEnd.Low:s.WrapperEnd.High] +} + +func (s *HtmlSummary) wrapLeft(ss string) string { + if s.WrapperStart.IsZero() { + return ss + } + + return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss +} + +func (s *HtmlSummary) Value(l types.LowHigh[string]) string { + return s.source[l.Low:l.High] +} + +func (s *HtmlSummary) trimSpace(ss string) string { + return strings.TrimSpace(ss) +} + +func (s *HtmlSummary) Content() string { + if s.Divider.IsZero() { + return s.source + } + ss := s.source[:s.Divider.Low] + ss += s.source[s.Divider.High:] + return s.trimSpace(ss) +} + +func (s *HtmlSummary) Summary() string { + if s.Divider.IsZero() { + return s.trimSpace(s.wrap(s.Value(s.SummaryLowHigh))) + } + ss := s.source[s.SummaryLowHigh.Low:s.Divider.Low] + if s.SummaryLowHigh.High > s.Divider.High { + ss += s.source[s.Divider.High:s.SummaryLowHigh.High] + } + if !s.SummaryEndTag.IsZero() { + ss += s.Value(s.SummaryEndTag) + } + return s.trimSpace(s.wrap(ss)) +} + +func (s *HtmlSummary) ContentWithoutSummary() string { + if s.Divider.IsZero() { + if s.SummaryLowHigh.Low == s.WrapperStart.High && s.SummaryLowHigh.High == s.WrapperEnd.Low { + return "" + } + return s.trimSpace(s.wrapLeft(s.source[s.SummaryLowHigh.High:])) + } + if s.SummaryEndTag.IsZero() { + return s.trimSpace(s.wrapLeft(s.source[s.Divider.High:])) + } + return s.trimSpace(s.wrapLeft(s.source[s.SummaryEndTag.High:])) +} + +func (s *HtmlSummary) Truncated() bool { + return s.SummaryLowHigh.High < len(s.source) +} + +func (s *HtmlSummary) resolveParagraphTagAndSetWrapper(mt media.Type) tagReStartEnd { + ptag := startEndP + + switch mt.SubType { + case DefaultContentTypes.AsciiDoc.SubType: + ptag = startEndDiv + case DefaultContentTypes.ReStructuredText.SubType: + const markerStart = "
" + const markerEnd = "
" + i1 := strings.Index(s.source, markerStart) + i2 := strings.LastIndex(s.source, markerEnd) + if i1 > -1 && i2 > -1 { + s.WrapperStart = types.LowHigh[string]{Low: 0, High: i1 + len(markerStart)} + s.WrapperEnd = types.LowHigh[string]{Low: i2, High: len(s.source)} + } + } + return ptag +} + +var ( + pOrDiv = regexp.MustCompile(`]?>|]?>$`) + + startEndDiv = tagReStartEnd{ + startEndOfString: regexp.MustCompile(`]*?>$`), + endEndOfString: regexp.MustCompile(`$`), + tagName: "div", + } + + startEndP = tagReStartEnd{ + startEndOfString: regexp.MustCompile(`]*?>$`), + endEndOfString: regexp.MustCompile(`

$`), + tagName: "p", + } +) + +type tagReStartEnd struct { + startEndOfString *regexp.Regexp + endEndOfString *regexp.Regexp + tagName string +} diff --git a/internal/domain/fs/entity/walk.go b/internal/domain/fs/entity/walk.go index a75bcc1d..e5719543 100644 --- a/internal/domain/fs/entity/walk.go +++ b/internal/domain/fs/entity/walk.go @@ -18,6 +18,10 @@ func (f *Fs) WalkLayouts(start string, cb fs.WalkCallback, conf fs.WalkwayConfig return f.Walk(f.Layouts, start, cb, conf) } +func (f *Fs) WalkI18n(start string, cb fs.WalkCallback, conf fs.WalkwayConfig) error { + return f.Walk(f.I18n, start, cb, conf) +} + func (f *Fs) Walk(fs afero.Fs, start string, cb fs.WalkCallback, conf fs.WalkwayConfig) error { w, err := valueobject.NewWalkway(fs, cb) if err != nil { diff --git a/internal/domain/resources/entity/publisher.go b/internal/domain/resources/entity/publisher.go index 9b1298c1..2b2b633f 100644 --- a/internal/domain/resources/entity/publisher.go +++ b/internal/domain/resources/entity/publisher.go @@ -1,6 +1,7 @@ package entity import ( + "github.com/gohugonet/hugoverse/internal/domain/resources" "github.com/gohugonet/hugoverse/internal/domain/resources/valueobject" "github.com/gohugonet/hugoverse/pkg/helpers" "github.com/spf13/afero" @@ -8,7 +9,8 @@ import ( ) type Publisher struct { - PubFs afero.Fs + PubFs afero.Fs + URLSvc resources.URLConfig } func (p *Publisher) PublishContentToTarget(content, target string) error { @@ -24,6 +26,6 @@ func (p *Publisher) PublishContentToTarget(content, target string) error { } func (p *Publisher) OpenPublishFileForWriting(relTargetPath string) (io.WriteCloser, error) { - filenames := valueobject.NewResourcePaths(relTargetPath).TargetFilenames() + filenames := valueobject.NewResourcePaths(relTargetPath, p.URLSvc).TargetFilenames() return helpers.OpenFilesForWriting(p.PubFs, filenames...) } diff --git a/internal/domain/resources/entity/resbuilder.go b/internal/domain/resources/entity/resbuilder.go index aa9948db..e8417c11 100644 --- a/internal/domain/resources/entity/resbuilder.go +++ b/internal/domain/resources/entity/resbuilder.go @@ -11,7 +11,6 @@ import ( "github.com/gohugonet/hugoverse/pkg/paths" "mime" "os" - "path" ) type resourceBuilder struct { @@ -27,6 +26,7 @@ type resourceBuilder struct { cache *Cache imageSvc resources.ImageConfig imageProc *valueobject.ImageProcessor + urlSvc resources.URLConfig } func newResourceBuilder(relPathname string, openReadSeekCloser io.OpenReadSeekCloser) *resourceBuilder { @@ -46,6 +46,11 @@ func (rs *resourceBuilder) withImageService(imageSvc resources.ImageConfig) *res return rs } +func (rs *resourceBuilder) withURLService(svc resources.URLConfig) *resourceBuilder { + rs.urlSvc = svc + return rs +} + func (rs *resourceBuilder) withImageProcessor(imageProc *valueobject.ImageProcessor) *resourceBuilder { rs.imageProc = imageProc return rs @@ -83,20 +88,7 @@ func (rs *resourceBuilder) build() (resources.Resource, error) { func (rs *resourceBuilder) buildResPaths() error { rs.relPathname = paths.ToSlashPreserveLeading(rs.relPathname) - - dir, name := path.Split(rs.relPathname) - dir = paths.ToSlashPreserveLeading(dir) - if dir == "/" { - dir = "" - } - - rs.resPaths = valueobject.ResourcePaths{ - Dir: dir, - BaseDirLink: "", - BaseDirTarget: "", - - File: name, - } + rs.resPaths = valueobject.NewResourcePaths(rs.relPathname, rs.urlSvc) return nil } @@ -155,6 +147,7 @@ func (rs *resourceBuilder) buildResource() (resources.Resource, error) { publisher: rs.publisher, mediaSvc: rs.mediaSvc, + urlSvc: rs.urlSvc, resourceTransformations: &resourceTransformations{}, diff --git a/internal/domain/resources/entity/resource.go b/internal/domain/resources/entity/resource.go index 48cebaa9..175d6af5 100644 --- a/internal/domain/resources/entity/resource.go +++ b/internal/domain/resources/entity/resource.go @@ -78,6 +78,7 @@ func (l *Resource) publish() { fmt.Println("publish ReadSeekCloser", l.paths.TargetPath(), err) return } + defer r.Close() _, err = io.Copy(publicw, r) if err != nil { diff --git a/internal/domain/resources/entity/resources.go b/internal/domain/resources/entity/resources.go index 9e268028..8efa1d19 100644 --- a/internal/domain/resources/entity/resources.go +++ b/internal/domain/resources/entity/resources.go @@ -28,6 +28,8 @@ type Resources struct { ImageService resources.ImageConfig ImageProc *valueobject.ImageProcessor + URLService resources.URLConfig + *MinifierClient *TemplateClient *IntegrityClient @@ -46,7 +48,7 @@ func (rs *Resources) GetResourceWithOpener(pathname string, opener io.OpenReadSe rsb := newResourceBuilder(pathname, opener) rsb.withCache(rs.Cache).withMediaService(rs.MediaService). withImageService(rs.ImageService).withImageProcessor(rs.ImageProc). - withPublisher(rs.Publisher) + withPublisher(rs.Publisher).withURLService(rs.URLService) return rsb.build() }) @@ -77,7 +79,7 @@ func (rs *Resources) GetResource(pathname string) (resources.Resource, error) { }) rsb.withCache(rs.Cache).withMediaService(rs.MediaService). withImageService(rs.ImageService).withImageProcessor(rs.ImageProc). - withPublisher(rs.Publisher) + withPublisher(rs.Publisher).withURLService(rs.URLService) return rsb.build() }) @@ -107,7 +109,7 @@ func (rs *Resources) match(name, pattern string, matchFunc func(r resources.Reso }) rsb.withCache(rs.Cache).withMediaService(rs.MediaService). withImageService(rs.ImageService).withImageProcessor(rs.ImageProc). - withPublisher(rs.Publisher) + withPublisher(rs.Publisher).withURLService(rs.URLService) r, err := rsb.build() if err != nil { diff --git a/internal/domain/resources/entity/resourcetransformer.go b/internal/domain/resources/entity/resourcetransformer.go index 88eb9785..73fdf30c 100644 --- a/internal/domain/resources/entity/resourcetransformer.go +++ b/internal/domain/resources/entity/resourcetransformer.go @@ -20,6 +20,7 @@ type ResourceTransformer struct { publisher *Publisher mediaSvc resources.MediaTypesConfig + urlSvc resources.URLConfig TransformationCache *Cache @@ -49,6 +50,7 @@ func (r *ResourceTransformer) TransformWithContext(ctx context.Context, t ...Res Resource: *res, publisher: r.publisher, mediaSvc: r.mediaSvc, + urlSvc: r.urlSvc, TransformationCache: r.TransformationCache, resourceTransformations: &resourceTransformations{}, }, nil @@ -97,10 +99,15 @@ func (r *ResourceTransformer) getFromFile(key string) (*Resource, error) { r2 := r.Resource.clone() r2.mediaType = m - r2.paths = valueobject.NewResourcePaths(meta.Target) + r2.paths = valueobject.NewResourcePaths(meta.Target, r.urlSvc) r2.mergeData(meta.MetaData) + + content, err := io.ReadAll(f) + if err != nil { + return nil, err + } r2.openReadSeekCloser = func() (pio.ReadSeekCloser, error) { - return f.(pio.ReadSeekCloser), nil + return pio.NewReadSeekerNoOpCloserFromString(string(content)), nil } return r2, nil @@ -162,7 +169,7 @@ func (r *ResourceTransformer) transform(key string) (*Resource, error) { updates.mediaType = tctx.Source.InMediaType updates.data = tctx.Data - updates.paths = valueobject.NewResourcePaths(tctx.Source.InPath) + updates.paths = valueobject.NewResourcePaths(tctx.Source.InPath, r.urlSvc) var publishwriters []io.WriteCloser //publicw, err := r.publisher.OpenPublishFileForWriting(updates.paths.TargetPath()) diff --git a/internal/domain/resources/factory/resource.go b/internal/domain/resources/factory/resource.go index 3b7dea7e..35220aa8 100644 --- a/internal/domain/resources/factory/resource.go +++ b/internal/domain/resources/factory/resource.go @@ -46,8 +46,11 @@ func NewResources(ws resources.Workspace) (*entity.Resources, error) { } rs := &entity.Resources{ - Cache: c, - Publisher: &entity.Publisher{PubFs: ws.PublishFs()}, + Cache: c, + Publisher: &entity.Publisher{ + PubFs: ws.PublishFs(), + URLSvc: ws, + }, FsService: ws, MediaService: ws, @@ -55,6 +58,8 @@ func NewResources(ws resources.Workspace) (*entity.Resources, error) { ImageService: ws, ImageProc: ip, + URLService: ws, + ExecHelper: execHelper, Common: common, diff --git a/internal/domain/resources/type.go b/internal/domain/resources/type.go index bd1cb075..729591fc 100644 --- a/internal/domain/resources/type.go +++ b/internal/domain/resources/type.go @@ -26,6 +26,11 @@ type Workspace interface { SecurityConfig OutputFormatsConfig MinifyConfig + URLConfig +} + +type URLConfig interface { + BaseUrl() string } type OutputFormatsConfig interface { diff --git a/internal/domain/resources/valueobject/resourcepaths.go b/internal/domain/resources/valueobject/resourcepaths.go index 7bb9dd94..4cf95244 100644 --- a/internal/domain/resources/valueobject/resourcepaths.go +++ b/internal/domain/resources/valueobject/resourcepaths.go @@ -14,6 +14,7 @@ package valueobject import ( + "github.com/gohugonet/hugoverse/internal/domain/resources" "github.com/gohugonet/hugoverse/pkg/paths" "path" "path/filepath" @@ -40,8 +41,9 @@ type ResourcePaths struct { File string } -func NewResourcePaths(targetPath string) ResourcePaths { +func NewResourcePaths(targetPath string, svc resources.URLConfig) ResourcePaths { targetPath = filepath.ToSlash(targetPath) + dir, file := path.Split(targetPath) dir = paths.ToSlashPreserveLeading(dir) if dir == "/" { @@ -51,8 +53,8 @@ func NewResourcePaths(targetPath string) ResourcePaths { return ResourcePaths{ Dir: dir, File: file, - BaseDirLink: "", - BaseDirTarget: "", + BaseDirLink: svc.BaseUrl(), + BaseDirTarget: svc.BaseUrl(), } } diff --git a/internal/domain/site/entity/hugo.go b/internal/domain/site/entity/hugo.go new file mode 100644 index 00000000..a29b25dc --- /dev/null +++ b/internal/domain/site/entity/hugo.go @@ -0,0 +1,29 @@ +package entity + +func (p *Page) LinkTitle() string { + return p.Title() +} + +func (p *Page) Sites() *Sites { + return &Sites{site: p.Site} +} + +type Sites struct { + site *Site +} + +func (s *Sites) First() *Site { + return s.site +} + +func (s *Sites) Default() *Site { + return s.First() +} + +func (s *Site) IsMultilingual() bool { + return s.IsMultiLingual() +} + +func (s *Site) IsMultihost() bool { + return false +} diff --git a/internal/domain/site/entity/pagefields.go b/internal/domain/site/entity/pagefields.go index 63a808bd..7e7ef8e9 100644 --- a/internal/domain/site/entity/pagefields.go +++ b/internal/domain/site/entity/pagefields.go @@ -20,7 +20,7 @@ func (p *Page) Resources() PageResources { } func (p *Page) Date() time.Time { - return time.Now() + return p.Page.PageDate() } func (p *Page) PublishDate() time.Time { @@ -44,18 +44,6 @@ func (p *Page) OutputFormats() valueobject.OutputFormats { return make(valueobject.OutputFormats, 0) } -func (p *Page) Sites() *sites { - return &sites{site: p.Site} -} - -type sites struct { - site *Site -} - -func (s *sites) First() *Site { - return s.site -} - func (p *Page) Data() any { p.dataInit.Do(func() { p.data = make(Data) diff --git a/internal/domain/site/entity/site.go b/internal/domain/site/entity/site.go index fe683ebb..580ab52f 100644 --- a/internal/domain/site/entity/site.go +++ b/internal/domain/site/entity/site.go @@ -12,10 +12,11 @@ import ( ) type Site struct { - ConfigSvc site.ConfigService - ContentSvc site.ContentService - ResourcesSvc site.ResourceService - LanguageSvc site.LanguageService + ConfigSvc site.ConfigService + ContentSvc site.ContentService + TranslationSvc site.TranslationService + ResourcesSvc site.ResourceService + LanguageSvc site.LanguageService GitSvc *valueobject.GitMap diff --git a/internal/domain/site/entity/sitefields.go b/internal/domain/site/entity/sitefields.go index 37a9bf8a..9bcb1d41 100644 --- a/internal/domain/site/entity/sitefields.go +++ b/internal/domain/site/entity/sitefields.go @@ -1,6 +1,7 @@ package entity import ( + "context" "fmt" "github.com/gohugonet/hugoverse/internal/domain/contenthub" "github.com/gohugonet/hugoverse/pkg/maps" @@ -123,3 +124,7 @@ func (s *Site) siteWeightedPage(p contenthub.OrdinalWeightPage) (*WeightedPage, return &WeightedPage{sp, p}, nil } + +func (s *Site) Translate(ctx context.Context, translationID string, templateData any) string { + return s.TranslationSvc.Translate(ctx, s.Language.currentLanguage, translationID, templateData) +} diff --git a/internal/domain/site/entity/url.go b/internal/domain/site/entity/url.go index c380f99a..c6c1e178 100644 --- a/internal/domain/site/entity/url.go +++ b/internal/domain/site/entity/url.go @@ -92,7 +92,7 @@ func (s *Site) RelURL(in string) string { u = s.URL.addContextRoot(u) u = s.URL.handleRootSuffix(in, u) - //u = s.URL.handlePrefix(u) // our use case include preview, just put it relative to the html file + u = s.URL.handlePrefix(u) // our use case include preview, just put it relative to the html file return u } diff --git a/internal/domain/site/factory/site.go b/internal/domain/site/factory/site.go index 7fc32a66..4ed76e27 100644 --- a/internal/domain/site/factory/site.go +++ b/internal/domain/site/factory/site.go @@ -18,10 +18,11 @@ func New(services site.Services) *entity.Site { } s := &entity.Site{ - ConfigSvc: services, - ContentSvc: services, - ResourcesSvc: services, - LanguageSvc: services, + ConfigSvc: services, + ContentSvc: services, + TranslationSvc: services, + ResourcesSvc: services, + LanguageSvc: services, GitSvc: git, diff --git a/internal/domain/site/type.go b/internal/domain/site/type.go index 38e11301..7bce0af4 100644 --- a/internal/domain/site/type.go +++ b/internal/domain/site/type.go @@ -16,6 +16,7 @@ import ( type Services interface { ContentService + TranslationService ResourceService LanguageService FsService @@ -53,6 +54,10 @@ type ContentService interface { GetPageRef(context contenthub.Page, ref string, home contenthub.Page) (contenthub.Page, error) } +type TranslationService interface { + Translate(ctx context.Context, lang string, translationID string, templateData any) string +} + type ResourceService interface { GetResourceWithOpener(pathname string, opener pio.OpenReadSeekCloser) (resources.Resource, error) } diff --git a/internal/domain/template/type.go b/internal/domain/template/type.go index 8769e8b5..89604bc7 100644 --- a/internal/domain/template/type.go +++ b/internal/domain/template/type.go @@ -6,6 +6,7 @@ import ( "github.com/gohugonet/hugoverse/pkg/template/funcs/collections" "github.com/gohugonet/hugoverse/pkg/template/funcs/compare" "github.com/gohugonet/hugoverse/pkg/template/funcs/hugo" + "github.com/gohugonet/hugoverse/pkg/template/funcs/lang" "github.com/gohugonet/hugoverse/pkg/template/funcs/os" "github.com/gohugonet/hugoverse/pkg/template/funcs/resource" "github.com/gohugonet/hugoverse/pkg/template/funcs/site" @@ -75,5 +76,6 @@ type CustomizedFunctions interface { resource.Resource os.Os site.Service - hugo.Version + hugo.Info + lang.Translator } diff --git a/internal/domain/template/valueobject/nscss.go b/internal/domain/template/valueobject/nscss.go new file mode 100644 index 00000000..d5db1e2d --- /dev/null +++ b/internal/domain/template/valueobject/nscss.go @@ -0,0 +1,32 @@ +package valueobject + +import ( + "context" + "github.com/gohugonet/hugoverse/pkg/template/funcs/resource" +) + +const nsCss = "css" + +func registerCss(res resource.Resource) { + f := func() *TemplateFuncsNamespace { + ctx, err := resource.New(res) + if err != nil { + // TODO(bep) no panic. + panic(err) + } + + ns := &TemplateFuncsNamespace{ + Name: nsCss, + Context: func(cctx context.Context, args ...any) (any, error) { return ctx, nil }, + } + + ns.AddMethodMapping(ctx.Sass, + []string{"toCSS"}, + [][2]string{}, + ) + + return ns + } + + AddTemplateFuncsNamespace(f) +} diff --git a/internal/domain/template/valueobject/nshugo.go b/internal/domain/template/valueobject/nshugo.go index cf6d357e..b028e765 100644 --- a/internal/domain/template/valueobject/nshugo.go +++ b/internal/domain/template/valueobject/nshugo.go @@ -7,12 +7,15 @@ import ( const nsHugo = "hugo" -func registerHugo(ver hugo.Version) { +func registerHugo(info hugo.Info) { f := func() *TemplateFuncsNamespace { + h := hugo.New(info) ns := &TemplateFuncsNamespace{ - Name: nsHugo, - Context: func(cctx context.Context, args ...any) (any, error) { return ver, nil }, + Name: nsHugo, + Context: func(cctx context.Context, args ...any) (any, error) { + return h, nil + }, } return ns diff --git a/internal/domain/template/valueobject/nslang.go b/internal/domain/template/valueobject/nslang.go index 0d41a3f2..bf584224 100644 --- a/internal/domain/template/valueobject/nslang.go +++ b/internal/domain/template/valueobject/nslang.go @@ -7,9 +7,9 @@ import ( const nsLang = "lang" -func registerLang() { +func registerLang(translator lang.Translator) { f := func() *TemplateFuncsNamespace { - ctx := lang.New() + ctx := lang.New(translator) ns := &TemplateFuncsNamespace{ Name: nsLang, diff --git a/internal/domain/template/valueobject/nsreg.go b/internal/domain/template/valueobject/nsreg.go index a6cf1f8b..98386588 100644 --- a/internal/domain/template/valueobject/nsreg.go +++ b/internal/domain/template/valueobject/nsreg.go @@ -19,7 +19,6 @@ func AddTemplateFuncsNamespace(ns func() *TemplateFuncsNamespace) { func RegisterNamespaces() { registerCast() registerFmt() - registerLang() registerSafe() registerCrypto() registerPath() @@ -36,11 +35,13 @@ func RegisterCallbackNamespaces(cb func(ctx context.Context, name string, data a } func RegisterExtendedNamespaces(functions template.CustomizedFunctions) { + registerLang(functions) registerCompare(functions) registerTransform(functions) registerUrls(functions, functions) registerStrings(functions) registerResources(functions) + registerCss(functions) registerOs(functions) registerSite(functions) registerHugo(functions) diff --git a/internal/domain/template/valueobject/nssite.go b/internal/domain/template/valueobject/nssite.go index 76b529cf..0ed14075 100644 --- a/internal/domain/template/valueobject/nssite.go +++ b/internal/domain/template/valueobject/nssite.go @@ -9,11 +9,7 @@ const nsSite = "site" func registerSite(svc site.Service) { f := func() *TemplateFuncsNamespace { - s, err := site.New(svc) - if err != nil { - // TODO(bep) no panic. - panic(err) - } + s := site.New(svc) ns := &TemplateFuncsNamespace{ Name: nsSite, diff --git a/pkg/media/builtin.go b/pkg/media/builtin.go index 97fb5576..01dd99af 100644 --- a/pkg/media/builtin.go +++ b/pkg/media/builtin.go @@ -34,8 +34,12 @@ type BuiltinTypes struct { OpenTypeFontType Type // Common document types - PDFType Type - MarkdownType Type + PDFType Type + MarkdownType Type + EmacsOrgModeType Type + AsciiDocType Type + PandocType Type + ReStructuredTextType Type // Common video types AVIType Type diff --git a/pkg/paths/pathparser.go b/pkg/paths/pathparser.go index 049093ba..a6f4f6c9 100644 --- a/pkg/paths/pathparser.go +++ b/pkg/paths/pathparser.go @@ -152,7 +152,7 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { } else { high = len(p.s) } - id := types.LowHigh{Low: i + 1, High: high} + id := types.LowHigh[string]{Low: i + 1, High: high} if len(p.identifiers) == 0 { p.identifiers = append(p.identifiers, id) } else if len(p.identifiers) == 1 { @@ -238,7 +238,7 @@ type Path struct { component string bundleType PathType - identifiers []types.LowHigh + identifiers []types.LowHigh[string] posIdentifierLanguage int disabled bool diff --git a/pkg/template/funcs/hugo/hugo.go b/pkg/template/funcs/hugo/hugo.go new file mode 100644 index 00000000..1224e8e2 --- /dev/null +++ b/pkg/template/funcs/hugo/hugo.go @@ -0,0 +1,13 @@ +package hugo + +// New returns a new instance of the os-namespaced template functions. +func New(info Info) *Namespace { + return &Namespace{ + Info: info, + } +} + +// Namespace provides template functions for the "os" namespace. +type Namespace struct { + Info +} diff --git a/pkg/template/funcs/hugo/type.go b/pkg/template/funcs/hugo/type.go index d5eaeead..3653e9c7 100644 --- a/pkg/template/funcs/hugo/type.go +++ b/pkg/template/funcs/hugo/type.go @@ -1,5 +1,24 @@ package hugo +type Info interface { + Version + Language + Host + Fs +} + type Version interface { Version() string } + +type Language interface { + IsMultilingual() bool +} + +type Host interface { + IsMultihost() bool +} + +type Fs interface { + WorkingDir() string +} diff --git a/pkg/template/funcs/lang/lang.go b/pkg/template/funcs/lang/lang.go index 545e78b9..024a1fba 100644 --- a/pkg/template/funcs/lang/lang.go +++ b/pkg/template/funcs/lang/lang.go @@ -30,28 +30,36 @@ import ( ) // New returns a new instance of the lang-namespaced template functions. -func New() *Namespace { +func New(svc Translator) *Namespace { return &Namespace{ - translator: translators.GetTranslator("en"), // TODO, make it more extensible + translator: translators.GetTranslator("en"), // TODO, make it more extensible + translationSvc: svc, } } // Namespace provides template functions for the "lang" namespace. type Namespace struct { - translator locales.Translator + translator locales.Translator + translationSvc Translator } // Translate returns a translated string for id. func (ns *Namespace) Translate(ctx context.Context, id any, args ...any) (string, error) { + var templateData any + + if len(args) > 0 { + if len(args) > 1 { + return "", fmt.Errorf("wrong number of arguments, expecting at most 2, got %d", len(args)+1) + } + templateData = args[0] + } + sid, err := cast.ToStringE(id) if err != nil { - return "", nil + return "", err } - // TODO, make it more extensible with real translator - // missing i18n, and translation provider - - return sid, nil + return ns.translationSvc.Translate(ctx, sid, templateData), nil } // FormatNumber formats number with the given precision for the current language. diff --git a/pkg/template/funcs/lang/type.go b/pkg/template/funcs/lang/type.go new file mode 100644 index 00000000..0fcc27e1 --- /dev/null +++ b/pkg/template/funcs/lang/type.go @@ -0,0 +1,7 @@ +package lang + +import "context" + +type Translator interface { + Translate(ctx context.Context, translationID string, templateData any) string +} diff --git a/pkg/template/funcs/resource/resources.go b/pkg/template/funcs/resource/resources.go index cf9154dc..da6449cb 100644 --- a/pkg/template/funcs/resource/resources.go +++ b/pkg/template/funcs/resource/resources.go @@ -124,6 +124,10 @@ func (ns *Namespace) Fingerprint(args ...any) (resources.Resource, error) { return ns.resourceService.Fingerprint(r, algo) } +func (ns *Namespace) Sass(args ...any) (resources.Resource, error) { + return ns.ToCSS(args...) +} + // ToCSS converts the given Resource to CSS. You can optional provide an Options object // as second argument. As an option, you can e.g. specify e.g. the target path (string) // for the converted CSS resource. diff --git a/pkg/template/funcs/site/site.go b/pkg/template/funcs/site/site.go index 23ba8558..1976085d 100644 --- a/pkg/template/funcs/site/site.go +++ b/pkg/template/funcs/site/site.go @@ -1,10 +1,10 @@ package site // New returns a new instance of the resources-namespaced template functions. -func New(svc Service) (*Namespace, error) { +func New(svc Service) *Namespace { return &Namespace{ Service: svc, - }, nil + } } // Namespace provides template functions for the "resources" namespace. diff --git a/pkg/types/types.go b/pkg/types/types.go index a0da0259..4787501b 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -51,7 +51,15 @@ func Unwrapv(v any) any { } // LowHigh is typically used to represent a slice boundary. -type LowHigh struct { +type LowHigh[S ~[]byte | string] struct { Low int High int } + +func (l LowHigh[S]) IsZero() bool { + return l.Low < 0 || (l.Low == 0 && l.High == 0) +} + +func (l LowHigh[S]) Value(source S) S { + return source[l.Low:l.High] +}