// Code generated by https://github.com/foomo/gocontentful - DO NOT EDIT. package testapi import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "reflect" "regexp" "strings" "sync" "time" "unicode" "github.com/foomo/contentful" "golang.org/x/net/html" "golang.org/x/sync/errgroup" ) type cacheEntryMaps struct { brand map[string]*CfBrand category map[string]*CfCategory product map[string]*CfProduct } type ClientMode string type ContentfulCache struct { assets assetCacheMap contentTypes []string entryMaps cacheEntryMaps genericEntries map[string]*GenericEntry idContentTypeMap map[string]string parentMap map[string][]EntryReference tags tagsCacheMap } type ContentfulCacheMutex struct { fullCacheGcLock sync.RWMutex sharedDataGcLock sync.RWMutex assetsGcLock sync.RWMutex tagGcLock sync.RWMutex idContentTypeMapGcLock sync.RWMutex parentMapGcLock sync.RWMutex genericEntriesGcLock sync.RWMutex brandGcLock sync.RWMutex categoryGcLock sync.RWMutex productGcLock sync.RWMutex } type assetCacheMap map[string]*contentful.Asset type tagsCacheMap map[string]string type ContentfulClient struct { Cache *ContentfulCache cacheInit bool cacheMutex *ContentfulCacheMutex cacheQueue chan struct{} cacheDone chan struct{} cacheUpdateTimeout int64 cacheWorkerOnce sync.Once clientMode ClientMode Client *contentful.Contentful locales []Locale logFn func( fields map[string]interface{}, level int, args ...interface{}, ) logLevel int optimisticPageSize uint16 SpaceID string offline bool offlineTemp offlineTemp sync bool syncToken string textJanitor bool } type offlineTemp struct { Entries []contentful.Entry `json:"entries"` Assets []contentful.Asset `json:"assets"` Tags []contentful.Tag `json:"tags"` } type ContentTypeResult struct { EntryID string ContentType string References map[string][]EntryReference } type ContentTypeInfo struct { ContentType string Title string Description string } type ContentTypeInfoMap map[string]ContentTypeInfo type EntryReference struct { ContentType string ID string VO interface{} CC *ContentfulClient FromField string } type entryOrRef interface { GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) } type BrokenReference struct { ParentID string `json:"parentId"` ParentType string `json:"parentType"` FromField string `json:"fromField"` ChildID string `json:"childId"` } type ImageResolverFunc func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error) type EntryLinkResolverFunc func(entryID string, locale Locale) (resolvedAttrs map[string]string, resolveError error) type LinkResolverFunc func(url string) (resolvedAttrs map[string]string, resolveError error) type EmbeddedEntryResolverFunc func(entryID string, locale Locale) (htmlSnippet string, resolveError error) type Locale string const ( ClientModeCDA ClientMode = "CDA" ClientModeCPA ClientMode = "CPA" ClientModeCMA ClientMode = "CMA" ) const ( LogDebug = 0 LogInfo = 1 LogWarn = 2 LogError = 3 ) const SpaceLocaleGerman Locale = "de" const SpaceLocaleFrench Locale = "fr" var SpaceLocales = []Locale{ "de", "fr", } const DefaultLocale Locale = SpaceLocaleGerman var localeFallback = map[Locale]Locale{SpaceLocaleGerman: "", SpaceLocaleFrench: ""} const ( assetPageSize = 1000 assetWorkerType = "_asset" tagWorkerType = "_tag" ) const cacheUpdateConcurrency = 4 const ( sysTypeEntry = "Entry" sysTypeAsset = "Asset" sysTypeDeletedEntry = "DeletedEntry" sysTypeDeletedAsset = "DeletedAsset" ) var ( ErrLocaleUnsupported = errors.New("locale not supported by this space") ErrNotSet = errors.New("field value not set") ErrNotSetNoFallback = errors.New("field value not set and no fallback locale available") ErrRefNotIncludes = errors.New("referenced entry not found in includes") ErrNoTypeOfRefEntry = errors.New("couldn't get contentType of referenced entry") ErrNoTypeOfRefAsset = errors.New("couldn't get contentType of referenced asset") InfoUpdatedEntityCache = "updated cache for entity" InfoCachedAllEntries = "cached all entries of content type" InfoCachedAllAssets = "cached all assets" InfoCachedAllTags = "cached all tags" InfoFallingBackToFile = "gonna use a local file" InfoLoadingFromFile = "loading space from local file" InfoCacheIsNil = "contentful cache is nil" InfoCacheWorkerStart = "contentful cache worker starting" InfoCacheUpdateQueued = "contentful cache update queued" InfoCacheUpdateCanceled = "contentful cache update canceled" InfoCacheUpdateDone = "contentful cache update returning" InfoCacheUpdateSkipped = "contentful cache update skipped, already one in the queue" InfoCacheSyncOp = "contentful cache sync op done" InfoOfflineEntitiesLoaded = "downloaded entries and assets from offline file" InfoPreservingExistingCache = "could not connect for cache update, preserving the existing cache" InfoUpdateCacheTime = "space caching done, time recorded" ErrorEnvironmentSetToMaster = "environment was empty string, set to master" ErrorEntryIsNil = "entry is nil" ErrorEntrySysIsNil = "entry.Sys is nil" ErrorEntryNotFound = "entry not found" ErrorEntrySysContentTypeIsNil = "entry.Sys.ContentType is nil" ErrorEntrySysContentTypeSysIsNil = "entry.Sys.ContentType.Sys is nil" ErrorEntryCachingFailed = "entry caching failed" ) var spaceContentTypes = []string{ContentTypeBrand, ContentTypeCategory, ContentTypeProduct} var SpaceContentTypeInfoMap = ContentTypeInfoMap{ "brand": ContentTypeInfo{ ContentType: "brand", Title: "Brand", Description: "", }, "category": ContentTypeInfo{ ContentType: "category", Title: "Category", Description: "", }, "product": ContentTypeInfo{ ContentType: "product", Title: "Product", Description: "", }, } func (cc *ContentfulClient) BrokenReferences() (brokenReferences []BrokenReference) { if cc.Cache == nil || cc.cacheMutex == nil { return } cc.cacheMutex.parentMapGcLock.Lock() defer cc.cacheMutex.parentMapGcLock.Unlock() cc.cacheMutex.idContentTypeMapGcLock.Lock() defer cc.cacheMutex.idContentTypeMapGcLock.Unlock() for childID, parents := range cc.Cache.parentMap { if _, okGotEntry := cc.Cache.idContentTypeMap[childID]; !okGotEntry { for _, parent := range parents { brokenReferences = append(brokenReferences, BrokenReference{ ParentID: parent.ID, ParentType: parent.ContentType, FromField: parent.FromField, ChildID: childID, }) } } } return } func (cc *ContentfulClient) CacheHasContentType(contentTypeID string) bool { if cc.Cache == nil { return false } cc.cacheMutex.sharedDataGcLock.RLock() defer cc.cacheMutex.sharedDataGcLock.RUnlock() for _, cachedContentType := range cc.Cache.contentTypes { if cachedContentType == contentTypeID { return true } } return false } func (cc *ContentfulClient) ClientStats() { if cc == nil { cc.logFn(nil, LogWarn, "ClientStats: no client available") return } cc.cacheMutex.sharedDataGcLock.RLock() defer cc.cacheMutex.sharedDataGcLock.RUnlock() if cc.logFn != nil { fieldsMap := map[string]interface{}{ "space ID": cc.SpaceID, "environment": cc.Client.Environment, "clientMode": cc.clientMode, "contentTypes": strings.Join(cc.Cache.contentTypes, ","), "locales": cc.locales, "cached": cc.cacheInit, } if cc.cacheInit { fieldsMap["cache asset count"] = len(cc.Cache.assets) fieldsMap["cache entry count"] = len(cc.Cache.idContentTypeMap) fieldsMap["cache parentMap children"] = len(cc.Cache.parentMap) cc.cacheMutex.parentMapGcLock.RLock() defer cc.cacheMutex.parentMapGcLock.RUnlock() referenceCount := 0 for _, parents := range cc.Cache.parentMap { referenceCount += len(parents) } fieldsMap["cache parentMap parents"] = referenceCount cc.cacheMutex.genericEntriesGcLock.RLock() defer cc.cacheMutex.genericEntriesGcLock.RUnlock() fieldsMap["cache genericEntries"] = len(cc.Cache.genericEntries) } cc.logFn(fieldsMap, LogInfo, "Contentful ClientStats") } } func (ref ContentfulReferencedEntry) ContentType() (contentType string) { return ref.Entry.Sys.ContentType.Sys.ID } func (cc *ContentfulClient) UpsertAsset(ctx context.Context, asset *contentful.Asset) error { if cc == nil || cc.Client == nil { return errors.New("UpsertAsset: No client available") } if cc.clientMode != ClientModeCMA { return errors.New("UpsertAsset: Only available in ClientModeCMA") } if asset.Fields != nil && asset.Fields.File != nil { for _, locale := range SpaceLocales { if asset.Fields.File[string(locale)] != nil { asset.Fields.File[string(locale)].URL = strings.ReplaceAll(asset.Fields.File[string(locale)].URL, "https:", "") } } } errUpsert := cc.Client.Assets.Upsert(ctx, cc.SpaceID, asset) if errUpsert != nil && !strings.Contains(errUpsert.Error(), "Not upserted") { return errUpsert } return nil } func (cc *ContentfulClient) PublishAsset(ctx context.Context, asset *contentful.Asset) error { if cc == nil || cc.Client == nil { return errors.New("PublishAsset: No client available") } if cc.clientMode != ClientModeCMA { return errors.New("PublishAsset: Only available in ClientModeCMA") } errPublish := cc.Client.Assets.Publish(ctx, cc.SpaceID, asset) if errPublish != nil && !strings.Contains(errPublish.Error(), "Not published") { return errPublish } return nil } func GetAssetPublishingStatus(asset *contentful.Asset) string { if asset == nil { return "" } if asset.Sys.PublishedVersion == 0 { return StatusDraft } if asset.Sys.Version-asset.Sys.PublishedVersion == 1 { return StatusPublished } return StatusChanged } func (cc *ContentfulClient) UpdateAsset(ctx context.Context, asset *contentful.Asset) (err error) { if asset == nil { return errors.New("UpdateAsset: Generic Entry is nil") } if cc == nil { return errors.New("UpdateAsset: Generic Entry has nil Contentful client") } if cc.clientMode != ClientModeCMA { return errors.New("UpdateAsset: Generic Entry not in ClientModeCMA") } publishingStatus := GetAssetPublishingStatus(asset) err = cc.UpsertAsset(ctx, asset) if err != nil { return fmt.Errorf("UpdateAsset: upsert operation failed: %w", err) } if publishingStatus == StatusPublished { err = cc.PublishAsset(ctx, asset) if err != nil { return fmt.Errorf("UpdateAsset: publish operation failed: %w", err) } } return } func (cc *ContentfulClient) DeleteAsset(ctx context.Context, asset *contentful.Asset) error { if cc == nil || cc.Client == nil { return errors.New("DeleteAsset: No client available") } if cc.clientMode != ClientModeCMA { return errors.New("DeleteAsset: Only available in ClientModeCMA") } errUnpublish := cc.Client.Assets.Unpublish(ctx, cc.SpaceID, asset) if errUnpublish != nil && !strings.Contains(errUnpublish.Error(), "Not published") { return errUnpublish } errDelete := cc.Client.Assets.Delete(ctx, cc.SpaceID, asset) if errDelete != nil { return errDelete } return nil } func (cc *ContentfulClient) DeleteAssetFromCache(key string) error { return cc.deleteAssetFromCache(key) } func (cc *ContentfulClient) DisableTextJanitor() { cc.textJanitor = false } func (cc *ContentfulClient) EnableTextJanitor() { cc.textJanitor = true } func (cc *ContentfulClient) GetAllAssets(ctx context.Context) (map[string]*contentful.Asset, error) { return cc.getAllAssets(ctx, true) } func (cc *ContentfulClient) GetAssetsByTag(ctx context.Context, tagName string) (vos []*contentful.Asset, err error) { if cc == nil || cc.Client == nil { return nil, errors.New("GetAssetsByTag: No client available") } if !cc.cacheInit { return nil, errors.New("GetAssetsByTag: only available with cache") } tags, err := cc.getAllTags(ctx, true) if err != nil { return nil, errors.New("GetAssetsByTag could not get tags from cache: " + err.Error()) } cc.cacheMutex.assetsGcLock.RLock() defer cc.cacheMutex.assetsGcLock.RUnlock() if _, tagExists := tags[tagName]; !tagExists { return nil, nil } tagID := tags[tagName] for _, vo := range cc.Cache.assets { for _, voTag := range vo.Metadata.Tags { if voTag.Sys.ID == tagID { vos = append(vos, vo) } } } return vos, nil } func (cc *ContentfulClient) GetAllTags(ctx context.Context) (map[string]string, error) { return cc.getAllTags(ctx, true) } func (cc *ContentfulClient) GetAssetByID(ctx context.Context, id string, forceNoCache ...bool) (*contentful.Asset, error) { if cc == nil || cc.Client == nil { return nil, errors.New("GetAssetByID: No client available") } cc.cacheMutex.sharedDataGcLock.Lock() cacheInit := cc.cacheInit cc.cacheMutex.sharedDataGcLock.Unlock() cc.cacheMutex.assetsGcLock.Lock() defer cc.cacheMutex.assetsGcLock.Unlock() if cacheInit && cc.Cache.assets != nil && (len(forceNoCache) == 0 || !forceNoCache[0]) { asset, okAsset := cc.Cache.assets[id] if okAsset { return asset, nil } else { return nil, errors.New("GetAssetByID: not found") } } col := cc.Client.Assets.List(ctx, cc.SpaceID) col.Query.Locale("*").Equal("sys.id", id) _, err := col.Next() if err != nil { return nil, err } if len(col.Items) == 0 { return nil, errors.New("GetAssetByID: Not found " + id) } item := col.Items[0] asset := contentful.Asset{} byt, err := json.Marshal(item) if err != nil { return nil, err } err = json.Unmarshal(byt, &asset) if err != nil { return nil, err } for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} { if _, ok := asset.Fields.File[string(loc)]; ok { asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL } } return &asset, nil } func (cc *ContentfulClient) GetContentTypeOfID(ctx context.Context, id string) (string, error) { if cc == nil || cc.Client == nil { return "", errors.New("GetContentTypeOfID: No client available") } cc.cacheMutex.sharedDataGcLock.RLock() cacheInit := cc.cacheInit cc.cacheMutex.sharedDataGcLock.RUnlock() if cacheInit { okVo := false cc.cacheMutex.brandGcLock.Lock() _, okVo = cc.Cache.entryMaps.brand[id] cc.cacheMutex.brandGcLock.Unlock() if okVo { return ContentTypeBrand, nil } cc.cacheMutex.categoryGcLock.Lock() _, okVo = cc.Cache.entryMaps.category[id] cc.cacheMutex.categoryGcLock.Unlock() if okVo { return ContentTypeCategory, nil } cc.cacheMutex.productGcLock.Lock() _, okVo = cc.Cache.entryMaps.product[id] cc.cacheMutex.productGcLock.Unlock() if okVo { return ContentTypeProduct, nil } return "", fmt.Errorf("GetContentTypeOfID: %s Not found in cache", id) } col := cc.Client.Entries.List(ctx, cc.SpaceID) col.Query.Include(0).Equal("sys.id", id) _, err := col.GetAll() if err != nil { return "", errors.New("GetContentTypeOfID: " + err.Error()) } if len(col.Items) == 0 { return "", fmt.Errorf("GetContentTypeOfID: %s Not found online", id) } var vo genericEntryNoFields byteArray, _ := json.Marshal(col.Items[0]) err = json.NewDecoder(bytes.NewReader(byteArray)).Decode(&vo) if err != nil { return "", errors.New("GetContentTypeOfID: " + err.Error()) } return vo.Sys.ContentType.Sys.ID, nil } func (cc *ContentfulClient) Locales() []Locale { return cc.locales } func (cc *ContentfulClient) LocalesAsStrings() []string { locales := []string{} for _, locale := range cc.locales { locales = append(locales, string(locale)) } return locales } func (vo *GenericEntry) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) { if vo == nil { return nil, errors.New("GetParents: Value Object is nil") } if vo.CC == nil { return nil, errors.New("GetParents: Value Object has no Contentful Client set") } return commonGetParents(ctx, vo.CC, vo.Sys.ID, contentType) } func (ref EntryReference) GetParents(ctx context.Context, contentType ...string) (parents []EntryReference, err error) { if ref.ID == "" { return nil, errors.New("GetParents: reference is nil") } var candidateParents []EntryReference var cc *ContentfulClient if ref.CC != nil { cc = ref.CC } else if ref.VO != nil { v := reflect.ValueOf(ref.VO) if v.Kind() == reflect.Ptr { if v.IsNil() { return nil, errors.New("GetParents: no client available, tried reflection to no avail") } v = v.Elem() } field := v.FieldByName("CC") if !field.IsValid() { return nil, errors.New("GetParents: VO has no CC field") } if field.Kind() != reflect.Ptr { return nil, errors.New("GetParents: VO field CC is not a pointer") } if field.IsNil() { return nil, errors.New("GetParents: VO field CC is nil") } var ok bool cc, ok = field.Interface().(*ContentfulClient) if !ok { return nil, errors.New("GetParents: VO field CC is not of type *ContentfulClient") } } if cc == nil { return nil, errors.New("GetParents: contentful client is nil") } if cc.Cache == nil { return nil, errors.New("GetParents: only available in cached mode") } if len(contentType) == 0 { return cc.Cache.parentMap[ref.ID], nil } else { cType := contentType[0] for _, candidateParent := range cc.Cache.parentMap[ref.ID] { if candidateParent.ContentType == cType { candidateParents = append(candidateParents, candidateParent) } } return candidateParents, nil } } func GetOrInheritFieldValue(ctx context.Context, contentfulShop *ContentfulClient, entryID string, field string, parentContentTypes []string, locale Locale) (any, error) { entry, err := contentfulShop.GetGenericEntry(entryID) if err != nil { return nil, fmt.Errorf("failed to get entry %s: %v", entryID, err) } // Try to get the field value from the current entry first val, err := entry.FieldAsAny(field, locale) if err == nil { return val, nil } return inheritFieldValueRecursive(ctx, contentfulShop, entry, field, parentContentTypes, locale, make(map[string]bool)) } func inheritFieldValueRecursive(ctx context.Context, contentfulShop *ContentfulClient, entry *GenericEntry, field string, parentContentTypes []string, locale Locale, visited map[string]bool) (any, error) { if visited[entry.Sys.ID] { return nil, fmt.Errorf("circular reference detected for entry %s", entry.Sys.ID) } visited[entry.Sys.ID] = true parentRefs, err := commonGetParents(ctx, entry.CC, entry.Sys.ID, parentContentTypes) if err != nil { return nil, fmt.Errorf("failed to get parents for entry %s: %v", entry.Sys.ID, err) } for _, parentRef := range parentRefs { parentEntry, err := entry.CC.GetGenericEntry(parentRef.ID) if err != nil { return nil, fmt.Errorf("failed to get parent entry %s: %v", parentRef.ID, err) } val, err := parentEntry.FieldAsAny(field, locale) if err == nil { return val, nil } inheritedVal, err := inheritFieldValueRecursive(ctx, contentfulShop, parentEntry, field, parentContentTypes, locale, visited) if err == nil { return inheritedVal, nil } } return nil, ErrNotSet } func HtmlToRichText(htmlSrc string) *RichTextNode { htmlClean := strings.TrimSpace(htmlSrc) htmlClean = strings.ReplaceAll(htmlClean, ` `, ` `) htmlClean = strings.ReplaceAll(htmlClean, "&", "&") htmlClean = strings.ReplaceAll(htmlClean, "\t", "") htmlClean = regexp.MustCompile(`[\s]{2,}`).ReplaceAllString(htmlClean, " ") re := regexp.MustCompile(`>([^\n])`) htmlClean = re.ReplaceAllString(htmlClean, fmt.Sprintf(">\n"+`$1`)) re = regexp.MustCompile(`(.{1})<`) htmlClean = re.ReplaceAllString(htmlClean, `$1`+"\n<") htmlLines := strings.Split(htmlClean, "\n") rtnd := &RichTextNode{ NodeType: RichTextNodeDocument, } var isBasic bool rtnd.Content, _, isBasic = richTextHtmlLinesToNode(htmlLines, 0, "", nil, true) if isBasic { basicRich := &RichTextNode{ NodeType: RichTextNodeDocument, Content: rtnd.Content, } return basicRich } return rtnd } func NewAssetFromURL(id string, uploadUrl string, imageFileType string, title string, locale ...Locale) *contentful.Asset { autoID := fmt.Sprintf("%d", time.Now().UnixNano()/int64(time.Millisecond)) if id == "" { id = autoID } if title == "" { title = id } var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return nil } } else { loc = DefaultLocale } asset := &contentful.Asset{ Sys: &contentful.Sys{ ID: id, }, Fields: &contentful.FileFields{ File: map[string]*contentful.File{ string(loc): { UploadURL: uploadUrl, ContentType: imageFileType, Name: id, }, }, Title: map[string]string{ string(loc): title, }, }, } return asset } func NewContentfulClient(ctx context.Context, spaceID string, clientMode ClientMode, clientKey string, optimisticPageSize uint16, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, debug bool) (*ContentfulClient, error) { if spaceID == "" { return nil, errors.New("NewContentfulClient: SpaceID cannot be empty") } if clientMode != ClientModeCMA && clientMode != ClientModeCPA && clientMode != ClientModeCDA { return nil, errors.New("NewContentfulClient: clientMode not supported") } if optimisticPageSize < 10 { return nil, errors.New("NewContentfulClient: optimisticPageSize must be 10 or bigger") } if logLevel < 0 || logLevel > 3 { return nil, errors.New("NewContentfulClient: logLevel must be between 0 and 3") } if clientKey == "" { return nil, errors.New("NewContentfulClient: Please provide an API key") } apiClient, err := getContentfulAPIClient(clientMode, clientKey) if err != nil { return nil, err } apiClient.Debug = debug cc := &ContentfulClient{ clientMode: clientMode, Client: apiClient, Cache: &ContentfulCache{ assets: assetCacheMap{}, contentTypes: []string{}, entryMaps: cacheEntryMaps{}, genericEntries: map[string]*GenericEntry{}, idContentTypeMap: map[string]string{}, parentMap: map[string][]EntryReference{}, }, cacheMutex: &ContentfulCacheMutex{}, cacheQueue: make(chan struct{}, 1), cacheDone: make(chan struct{}, 1), cacheUpdateTimeout: 120, locales: []Locale{SpaceLocaleGerman, SpaceLocaleFrench}, logFn: logFn, logLevel: logLevel, optimisticPageSize: optimisticPageSize, SpaceID: spaceID, sync: false, } _, err = cc.Client.Spaces.Get(ctx, spaceID) if err != nil { _, ok := err.(contentful.NotFoundError) if ok { return nil, errors.New("NewContentfulClient: That is not the space you're looking for") } return nil, errors.New("NewContentfulClient: " + err.Error()) } return cc, nil } func NewOfflineContentfulClient(file []byte, logFn func(fields map[string]interface{}, level int, args ...interface{}), logLevel int, cacheAssets bool, textJanitor bool) (*ContentfulClient, error) { offlineTemp, err := getOfflineSpaceFromFile(file) if err != nil { return nil, fmt.Errorf("NewOfflineContentfulClient could not parse space export file: %v", err) } cc := &ContentfulClient{ clientMode: ClientModeCDA, Client: contentful.NewCDA(""), Cache: &ContentfulCache{ contentTypes: []string{}, idContentTypeMap: map[string]string{}, genericEntries: map[string]*GenericEntry{}, parentMap: map[string][]EntryReference{}, }, cacheMutex: &ContentfulCacheMutex{}, cacheQueue: make(chan struct{}, 1), cacheDone: make(chan struct{}, 1), cacheUpdateTimeout: 120, locales: []Locale{ SpaceLocaleGerman, SpaceLocaleFrench, }, logFn: logFn, logLevel: logLevel, SpaceID: "OFFLINE", offline: true, offlineTemp: *offlineTemp, textJanitor: textJanitor, } if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"entries": len(offlineTemp.Entries), "assets": len(offlineTemp.Assets)}, LogInfo, InfoLoadingFromFile) } _, _, err = cc.UpdateCache(context.Background(), spaceContentTypes, cacheAssets) if err != nil { return nil, fmt.Errorf("NewOfflineContentfulClient could not cache offline space: %v", err) } return cc, nil } func RichTextToHtml(rt interface{}, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (string, error) { w := bytes.NewBuffer([]byte{}) node := &RichTextGenericNode{} byt, err := json.Marshal(rt) if err != nil { return "", err } err = json.Unmarshal(byt, node) if err != nil { return "", err } err = node.richTextRenderHTML(w, linkResolver, entryLinkResolver, imageResolver, embeddedEntryResolver, locale) if err != nil { return "", err } out := w.String() if out == "
" { return "", nil } return out, nil } func RichTextToPlainText(rt interface{}, locale Locale) (string, error) { htmlStr, err := RichTextToHtml(rt, nil, nil, nil, nil, locale) if err != nil { return "", err } doc, err := html.Parse(strings.NewReader(htmlStr)) if err != nil { return "", err } var f func(*html.Node) text := "" f = func(n *html.Node) { if n.Type == html.TextNode { text += n.Data } for c := n.FirstChild; c != nil; c = c.NextSibling { f(c) } } f(doc) return strings.TrimSpace(text), nil } func (cc *ContentfulClient) GetGenericEntry(entryID string) (*GenericEntry, error) { if cc.Cache == nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"task": "GetGenericEntry", "entryId": entryID}, LogWarn, InfoCacheIsNil) } return nil, errors.New(InfoCacheIsNil) } if _, ok := cc.Cache.genericEntries[entryID]; !ok { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"task": "GetGenericEntry", "entryId": entryID}, LogWarn, ErrorEntryNotFound) } return nil, errors.New(ErrorEntryNotFound) } return cc.Cache.genericEntries[entryID], nil } func (cc *ContentfulClient) GetAllGenericEntries() (map[string]*GenericEntry, error) { if cc.Cache == nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"task": "GetAllGenericEntries"}, LogWarn, InfoCacheIsNil) } return nil, errors.New(InfoCacheIsNil) } return cc.Cache.genericEntries, nil } func (genericEntry *GenericEntry) FieldAsString(fieldName string, locale ...Locale) (string, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return "", ErrLocaleUnsupported } } else { loc = DefaultLocale } if _, ok := genericEntry.RawFields[fieldName]; !ok { return "", ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: var fieldLoc any if _, ok := field[string(loc)]; ok { fieldLoc = field[string(loc)] } else { fieldLoc = field[string(DefaultLocale)] } switch fieldLocStr := fieldLoc.(type) { case string: return fieldLocStr, nil default: return "", ErrNotSet } default: return "", ErrNotSet } } func (genericEntry *GenericEntry) InheritAsString(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (string, error) { val, err := genericEntry.FieldAsString(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return "", err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return "", err } parentVal, err := genericParent.FieldAsString(fieldName, locale...) if err != nil { return "", err } else { return parentVal, nil } } } return "", ErrNotSet } func (genericEntry *GenericEntry) FieldAsStringSlice(fieldName string, locale ...Locale) ([]string, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return nil, ErrLocaleUnsupported } } else { loc = DefaultLocale } if _, ok := genericEntry.RawFields[fieldName]; !ok { return nil, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]any: var fieldLoc any if _, ok := field[string(loc)]; ok { fieldLoc = field[string(loc)] } else { fieldLoc = field[string(DefaultLocale)] } if fieldLoc != nil { switch fieldLoc.(type) { case []any: var out []string for _, v := range fieldLoc.([]any) { switch v.(type) { case string: out = append(out, v.(string)) } } return out, nil } } } return nil, ErrNotSet } func (genericEntry *GenericEntry) InheritAsStringSlice(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) ([]string, error) { val, err := genericEntry.FieldAsStringSlice(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return nil, err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return nil, err } parentVal, err := genericParent.FieldAsStringSlice(fieldName, locale...) if err != nil { return nil, err } else { return parentVal, nil } } } return nil, ErrNotSet } func (genericEntry *GenericEntry) FieldAsFloat64(fieldName string, locale ...Locale) (float64, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return 0, ErrLocaleUnsupported } } else { loc = DefaultLocale } if _, ok := genericEntry.RawFields[fieldName]; !ok { return 0, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: fieldLoc := field[string(loc)] if fieldLoc == 0 && field[string(DefaultLocale)] != 0 { fieldLoc = field[string(DefaultLocale)] } switch fieldLocFloat := fieldLoc.(type) { case float64: return fieldLocFloat, nil default: return 0, ErrNotSet } default: return 0, ErrNotSet } } func (genericEntry *GenericEntry) InheritAsFloat64(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (float64, error) { val, err := genericEntry.FieldAsFloat64(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return 0, err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return 0, err } parentVal, err := genericParent.FieldAsFloat64(fieldName, locale...) if err != nil { return 0, err } else { return parentVal, nil } } } return 0, ErrNotSet } func (genericEntry *GenericEntry) FieldAsReference(fieldName string, locale ...Locale) (*EntryReference, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return nil, ErrLocaleUnsupported } } else { loc = DefaultLocale } var cts ContentTypeSys if _, ok := genericEntry.RawFields[fieldName]; !ok { return nil, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: fieldVal := field[string(loc)] if fieldVal == nil { fieldVal = field[string(DefaultLocale)] } byt, err := json.Marshal(fieldVal) if err != nil { return nil, err } err = json.Unmarshal(byt, &cts) if err != nil { return nil, err } if cts.Sys.ID == "" { return nil, errors.New("not a reference") } referencedEntry, err := genericEntry.CC.GetGenericEntry(cts.Sys.ID) if err != nil || referencedEntry == nil { return nil, err } ref := EntryReference{ ID: cts.Sys.ID, ContentType: referencedEntry.Sys.ContentType.Sys.ID, FromField: fieldName, } return &ref, nil } return nil, ErrNotSet } func (genericEntry *GenericEntry) InheritAsReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (*EntryReference, error) { val, err := genericEntry.FieldAsReference(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return nil, err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return nil, err } parentVal, err := genericParent.FieldAsReference(fieldName, locale...) if err != nil { return nil, err } else { return parentVal, nil } } } return nil, ErrNotSet } func (genericEntry *GenericEntry) FieldAsAsset(ctx context.Context, fieldName string, locale ...Locale) (*contentful.AssetNoLocale, error) { var loc Locale reqLoc := DefaultLocale if len(locale) != 0 { loc = locale[0] reqLoc = locale[0] if _, ok := localeFallback[loc]; !ok { return nil, ErrLocaleUnsupported } } else { loc = DefaultLocale } var cts ContentTypeSys if _, ok := genericEntry.RawFields[fieldName]; !ok { return nil, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: fieldVal := field[string(loc)] if fieldVal == nil { fieldVal = field[string(DefaultLocale)] } byt, err := json.Marshal(fieldVal) if err != nil { return nil, err } err = json.Unmarshal(byt, &cts) if err != nil { return nil, err } asset, err := genericEntry.CC.GetAssetByID(ctx, cts.Sys.ID) if err != nil { if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel == LogDebug { genericEntry.CC.logFn(map[string]interface{}{"content type": genericEntry.Sys.ContentType.Sys.ID, "entry ID": genericEntry.Sys.ID, "method": "HeaderImage()"}, LogError, ErrNoTypeOfRefAsset) } return nil, ErrNotSet } tempAsset := &contentful.AssetNoLocale{} tempAsset.Sys = asset.Sys tempAsset.Fields = &contentful.FileFieldsNoLocale{} if _, ok := asset.Fields.Title[string(reqLoc)]; ok { tempAsset.Fields.Title = asset.Fields.Title[string(reqLoc)] } else { tempAsset.Fields.Title = asset.Fields.Title[string(loc)] } if _, ok := asset.Fields.Description[string(reqLoc)]; ok { tempAsset.Fields.Description = asset.Fields.Description[string(reqLoc)] } else { tempAsset.Fields.Description = asset.Fields.Description[string(loc)] } if _, ok := asset.Fields.File[string(reqLoc)]; ok { tempAsset.Fields.File = asset.Fields.File[string(reqLoc)] } else { tempAsset.Fields.File = asset.Fields.File[string(loc)] } return tempAsset, nil } return nil, ErrNotSet } func (genericEntry *GenericEntry) FieldAsMultipleReference(fieldName string, locale ...Locale) ([]*EntryReference, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return nil, ErrLocaleUnsupported } } else { loc = DefaultLocale } var ctss []ContentTypeSys if _, ok := genericEntry.RawFields[fieldName]; !ok { return nil, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: fieldVal := field[string(loc)] if fieldVal == nil { fieldVal = field[string(DefaultLocale)] } byt, err := json.Marshal(fieldVal) if err != nil { return nil, err } err = json.Unmarshal(byt, &ctss) if err != nil { return nil, err } var refs []*EntryReference for _, cts := range ctss { if cts.Sys.ID == "" { continue } referencedEntry, err := genericEntry.CC.GetGenericEntry(cts.Sys.ID) if err != nil || referencedEntry == nil { continue } refs = append(refs, &EntryReference{ ID: cts.Sys.ID, ContentType: referencedEntry.Sys.ContentType.Sys.ID, FromField: fieldName, }) } return refs, nil } return nil, ErrNotSet } func (genericEntry *GenericEntry) InheritAsMultipleReference(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) ([]*EntryReference, error) { val, err := genericEntry.FieldAsMultipleReference(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return nil, err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return nil, err } parentVal, err := genericParent.FieldAsMultipleReference(fieldName, locale...) if err != nil { return nil, err } else { return parentVal, nil } } } return nil, ErrNotSet } func (genericEntry *GenericEntry) FieldAsBool(fieldName string, locale ...Locale) (bool, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return false, ErrLocaleUnsupported } } else { loc = DefaultLocale } if _, ok := genericEntry.RawFields[fieldName]; !ok { return false, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: var fieldLoc any if _, ok := field[string(loc)]; ok { fieldLoc = field[string(loc)] } else { fieldLoc = field[string(DefaultLocale)] } switch fieldLocStr := fieldLoc.(type) { case bool: return fieldLocStr, nil default: return false, ErrNotSet } default: return false, ErrNotSet } } func (genericEntry *GenericEntry) InheritAsBool(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (bool, error) { val, err := genericEntry.FieldAsBool(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return false, err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return false, err } parentVal, err := genericParent.FieldAsBool(fieldName, locale...) if err != nil { return false, err } else { return parentVal, nil } } } return false, ErrNotSet } func (genericEntry *GenericEntry) FieldAsAny(fieldName string, locale ...Locale) (any, error) { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return nil, ErrLocaleUnsupported } } else { loc = DefaultLocale } if _, ok := genericEntry.RawFields[fieldName]; !ok { return nil, ErrNotSet } switch field := genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: var fieldLoc any if _, ok := field[string(loc)]; ok { fieldLoc = field[string(loc)] } else { fieldLoc = field[string(DefaultLocale)] } if fieldLoc != nil { return fieldLoc, nil } } return nil, ErrNotSet } func (genericEntry *GenericEntry) InheritAsAny(ctx context.Context, fieldName string, parentTypes []string, locale ...Locale) (any, error) { val, err := genericEntry.FieldAsAny(fieldName, locale...) if err == nil { return val, nil } parentRefs, err := commonGetParents(ctx, genericEntry.CC, genericEntry.Sys.ID, parentTypes) if err != nil { return nil, err } if len(parentRefs) > 0 { for _, parentRef := range parentRefs { genericParent, err := genericEntry.CC.GetGenericEntry(parentRef.ID) if err != nil { return nil, err } parentVal, err := genericParent.FieldAsAny(fieldName, locale...) if err != nil { return nil, err } else { return parentVal, nil } } } return nil, ErrNotSet } func (genericEntry *GenericEntry) SetField(fieldName string, fieldValue interface{}, locale ...Locale) error { var loc Locale if len(locale) != 0 { loc = locale[0] if _, ok := localeFallback[loc]; !ok { return ErrLocaleUnsupported } } else { loc = DefaultLocale } if genericEntry.RawFields == nil { genericEntry.RawFields = make(RawFields) } if genericEntry.RawFields[fieldName] == nil { genericEntry.RawFields[fieldName] = map[string]interface{}{} } switch genericEntry.RawFields[fieldName].(type) { case map[string]interface{}: genericEntry.RawFields[fieldName].(map[string]interface{})[string(loc)] = fieldValue return nil } return ErrNotSet } func (genericEntry *GenericEntry) Upsert(ctx context.Context) error { cfEntry := &contentful.Entry{ Fields: map[string]interface{}{}, } // get the generic entry sys into the cfEntry sys tmp, errMarshal := json.Marshal(genericEntry) if errMarshal != nil { return errors.New("can't marshal JSON from entry") } errUnmarshal := json.Unmarshal(tmp, &cfEntry) if errUnmarshal != nil { return errors.New("can't unmarshal JSON into CF entry") } // copy fields for key, fieldValue := range genericEntry.RawFields { cfEntry.Fields[key] = fieldValue } // upsert the entry err := genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry) if err != nil { if genericEntry.CC.logFn != nil && genericEntry.CC.logLevel <= LogWarn { genericEntry.CC.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, fmt.Errorf("CfAkeneoSettings UpsertEntry: Operation failed: %w", err)) } return err } return nil } func (genericEntry *GenericEntry) Update(ctx context.Context) (err error) { if genericEntry == nil { return errors.New("Update: Generic Entry is nil") } if genericEntry.CC == nil { return errors.New("Update: Generic Entry has nil Contentful client") } if genericEntry.CC.clientMode != ClientModeCMA { return errors.New("Update: Generic Entry not in ClientModeCMA") } publishingStatus := genericEntry.GetPublishingStatus() cfEntry := &contentful.Entry{} tmp, errMarshal := json.Marshal(genericEntry) if errMarshal != nil { return errors.New("Update: Can't marshal JSON from VO") } errUnmarshal := json.Unmarshal(tmp, &cfEntry) if errUnmarshal != nil { return errors.New("Update: Can't unmarshal JSON into CF entry") } err = genericEntry.CC.Client.Entries.Upsert(ctx, genericEntry.CC.SpaceID, cfEntry) if err != nil { return fmt.Errorf("Update: upsert operation failed: %w", err) } tmp, errMarshal = json.Marshal(cfEntry) if errMarshal != nil { return errors.New("Update: Can't marshal JSON back from CF entry") } errUnmarshal = json.Unmarshal(tmp, &genericEntry) if errUnmarshal != nil { return errors.New("Update: Can't unmarshal JSON back into Generic Entry") } if publishingStatus == StatusPublished { genericEntry.Sys.Version++ err = genericEntry.CC.Client.Entries.Publish(ctx, genericEntry.CC.SpaceID, cfEntry) if err != nil { return fmt.Errorf("Update: publish operation failed: %w", err) } } return } func (genericEntry *GenericEntry) GetPublishingStatus() string { if genericEntry == nil { return "" } if genericEntry.Sys.PublishedVersion == 0 { return StatusDraft } if genericEntry.Sys.Version-genericEntry.Sys.PublishedVersion == 1 { return StatusPublished } return StatusChanged } func (cc *ContentfulClient) ClientMode() ClientMode { return cc.clientMode } func (cc *ContentfulClient) SetCacheUpdateTimeout(seconds int64) { cc.cacheUpdateTimeout = seconds } func (cc *ContentfulClient) SetEnvironment(environment string) { if environment == "" { cc.Client.Environment = "master" if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogWarn, ErrorEnvironmentSetToMaster) } return } cc.Client.Environment = environment } func (cc *ContentfulClient) SetOfflineFallback(file []byte) error { offlineTemp, err := getOfflineSpaceFromFile(file) if err != nil { return err } cc.offlineTemp = *offlineTemp return nil } func (cc *ContentfulClient) SetSyncMode(mode bool) error { cc.cacheMutex.sharedDataGcLock.Lock() defer cc.cacheMutex.sharedDataGcLock.Unlock() if cc.offline { return errors.New("SetSyncMode: client is set offline, can't enable sync") } cc.sync = mode return nil } func (cc *ContentfulClient) ResetSync() { cc.cacheMutex.sharedDataGcLock.Lock() defer cc.cacheMutex.sharedDataGcLock.Unlock() cc.syncToken = "" } func (cc *ContentfulClient) UpdateCache(ctx context.Context, contentTypes []string, cacheAssets bool) (map[string][]string, []string, error) { cc.cacheMutex.sharedDataGcLock.RLock() ctxAtWork, cancel := context.WithTimeout(ctx, time.Second*time.Duration(cc.cacheUpdateTimeout)) defer cancel() localOffline := cc.offline isSync := cc.sync cc.cacheMutex.sharedDataGcLock.RUnlock() if localOffline { ctxAtWork = ctx } if !localOffline { time.Sleep(time.Second * 2) } if contentTypes == nil { contentTypes = spaceContentTypes } else { for _, contentType := range contentTypes { if !stringSliceContains(spaceContentTypes, contentType) { return nil, nil, fmt.Errorf("UpdateCache: Content Type %q not available in this space", contentType) } } } tags, err := cc.GetAllTags(ctx) if err != nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"task": "UpdateCache", "error": err.Error()}, LogWarn, "failed to cache tags") } } cc.cacheMutex.tagGcLock.Lock() cc.Cache.tags = tags cc.cacheMutex.tagGcLock.Unlock() if isSync { return cc.syncCache(ctxAtWork, contentTypes) } cc.cacheWorkerOnce.Do(func() { go cc.cacheWorker(ctx, contentTypes, cacheAssets) }) if len(cc.cacheQueue) == 0 { if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateQueued) } cc.cacheQueue <- struct{}{} select { case <-cc.cacheDone: if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateDone) } case <-ctxAtWork.Done(): if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateCanceled) } } cc.cacheMutex.sharedDataGcLock.Lock() cc.cacheInit = true cc.cacheMutex.sharedDataGcLock.Unlock() if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateDone) } return nil, nil, nil } if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheUpdateSkipped) } return nil, nil, nil } func (cc *ContentfulClient) syncCache(ctx context.Context, contentTypes []string) (map[string][]string, []string, error) { start := time.Now() cc.cacheMutex.sharedDataGcLock.Lock() cc.Cache.contentTypes = contentTypes cc.cacheMutex.sharedDataGcLock.Unlock() if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "syncCache"}, LogInfo, InfoCacheUpdateQueued) } allTags, err := cc.getAllTags(ctx, false) if err != nil { return nil, nil, errors.New("syncCache failed for tags") } cc.cacheMutex.sharedDataGcLock.Lock() cc.Cache.tags = allTags cc.cacheMutex.sharedDataGcLock.Unlock() var syncdEntries map[string][]string var syncdAssets []string for { if ctx.Err() != nil { return nil, nil, ctx.Err() } cc.cacheMutex.sharedDataGcLock.RLock() col := cc.Client.Entries.Sync( ctx, cc.SpaceID, cc.syncToken == "", cc.syncToken, ) cc.cacheMutex.sharedDataGcLock.RUnlock() if _, err := col.GetAll(); err != nil { return nil, nil, err } cc.cacheMutex.sharedDataGcLock.Lock() cc.syncToken = col.SyncToken cc.cacheInit = true cc.cacheMutex.sharedDataGcLock.Unlock() if len(col.Items) == 0 { if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"time elapsed": fmt.Sprint(time.Since(start)), "task": "syncCache"}, LogInfo, InfoUpdateCacheTime) } return syncdEntries, syncdAssets, nil } var entries []*contentful.Entry var assets []*contentful.Asset for _, item := range col.Items { if ctx.Err() != nil { return nil, nil, ctx.Err() } entry := &contentful.Entry{} byteArray, _ := json.Marshal(item) errEntry := json.NewDecoder(bytes.NewReader(byteArray)).Decode(entry) if errEntry == nil && entry.Sys != nil && (entry.Sys.Type == sysTypeAsset || entry.Sys.Type == sysTypeDeletedAsset) { asset := &contentful.Asset{} errAsset := json.NewDecoder(bytes.NewReader(byteArray)).Decode(asset) if errAsset != nil { continue } if asset.Sys != nil { assets = append(assets, asset) } continue } if entry.Sys != nil { entries = append(entries, entry) } } for _, entry := range entries { if ctx.Err() != nil { return nil, nil, ctx.Err() } switch entry.Sys.Type { case sysTypeEntry: if !stringSliceContains(spaceContentTypes, entry.Sys.ContentType.Sys.ID) { continue } if err := updateCacheForContentTypeAndEntity(ctx, cc, entry.Sys.ContentType.Sys.ID, entry.Sys.ID, entry, false); err != nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry") } } else { if syncdEntries == nil { syncdEntries = map[string][]string{} } if _, ok := syncdEntries[entry.Sys.ContentType.Sys.ID]; !ok { syncdEntries[entry.Sys.ContentType.Sys.ID] = []string{} } syncdEntries[entry.Sys.ContentType.Sys.ID] = append(syncdEntries[entry.Sys.ContentType.Sys.ID], entry.Sys.ID) } case sysTypeDeletedEntry: cc.cacheMutex.idContentTypeMapGcLock.RLock() contentType := cc.Cache.idContentTypeMap[entry.Sys.ID] cc.cacheMutex.idContentTypeMapGcLock.RUnlock() if err := updateCacheForContentTypeAndEntity(ctx, cc, contentType, entry.Sys.ID, entry, true); err != nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"id": entry.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry") } } else { if syncdEntries == nil { syncdEntries = map[string][]string{} } if _, ok := syncdEntries[contentType]; !ok { syncdEntries[contentType] = []string{} } syncdEntries[contentType] = append(syncdEntries[contentType], entry.Sys.ID) } default: } } if cc.logFn != nil && len(entries) > 0 && cc.logLevel <= LogInfo { for contentType, ids := range syncdEntries { cc.logFn(map[string]interface{}{"task": "syncCache", "contentType": contentType, "syncEntryCount": len(ids)}, LogInfo, InfoCacheSyncOp) } } for _, asset := range assets { if ctx.Err() != nil { return nil, nil, ctx.Err() } switch asset.Sys.Type { case sysTypeAsset: if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, asset, false); err != nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to update cache for entry") } } else { syncdAssets = append(syncdAssets, asset.Sys.ID) } case sysTypeDeletedAsset: if err := updateCacheForContentTypeAndEntity(ctx, cc, assetWorkerType, asset.Sys.ID, nil, true); err != nil { if cc.logFn != nil && cc.logLevel <= LogWarn { cc.logFn(map[string]interface{}{"id": asset.Sys.ID, "task": "syncCache", "error": err.Error()}, LogWarn, "failed to delete cache for entry") } } else { syncdAssets = append(syncdAssets, asset.Sys.ID) } default: } } if cc.logFn != nil && len(assets) > 0 && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "syncCache", "syncAssetCount": len(syncdAssets)}, LogInfo, InfoCacheSyncOp) } } return syncdEntries, syncdAssets, nil } func (cc *ContentfulClient) cacheWorker(ctx context.Context, contentTypes []string, cacheAssets bool) { if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache"}, LogInfo, InfoCacheWorkerStart) } for range cc.cacheQueue { cc.cacheSpace(ctx, contentTypes, cacheAssets) cc.cacheDone <- struct{}{} } } func (cc *ContentfulClient) cacheSpace(ctx context.Context, contentTypes []string, cacheAssets bool) { start := time.Now() tempCache := &ContentfulCache{ contentTypes: contentTypes, idContentTypeMap: map[string]string{}, genericEntries: map[string]*GenericEntry{}, parentMap: map[string][]EntryReference{}, } if cacheAssets { contentTypes = append([]string{assetWorkerType, tagWorkerType}, contentTypes...) } _, errCanWeEvenConnect := cc.Client.Spaces.Get(ctx, cc.SpaceID) cc.cacheMutex.sharedDataGcLock.RLock() offlinePreviousState := cc.offline cc.cacheMutex.sharedDataGcLock.RUnlock() if errCanWeEvenConnect != nil { if len(cc.offlineTemp.Entries) > 0 && (cc.Cache == nil || offlinePreviousState) { if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoFallingBackToFile) } cc.cacheMutex.sharedDataGcLock.Lock() cc.offline = true cc.cacheMutex.sharedDataGcLock.Unlock() } else { if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoPreservingExistingCache) } } } results := make(chan ContentTypeResult, 16) resultsDone := make(chan struct{}) contentTypeChan := make(chan string) group, gctx := errgroup.WithContext(ctx) for i := 0; i < cacheUpdateConcurrency; i++ { group.Go(func() error { for contentType := range contentTypeChan { err := updateCacheForContentType(gctx, results, cc, tempCache, contentType) if err != nil { if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache", "contentType": contentType, "clientMode": cc.clientMode}, LogError, err.Error()) } return err } } return nil }) } go func() { for _, contentType := range contentTypes { contentTypeChan <- contentType } close(contentTypeChan) }() go func() { for res := range results { tempCache.idContentTypeMap[res.EntryID] = res.ContentType for childID, references := range res.References { tempCache.parentMap[childID] = append(tempCache.parentMap[childID], references...) } } resultsDone <- struct{}{} }() err := group.Wait() close(results) if err != nil { // drain contentTypeChan for range contentTypeChan { } cc.cacheMutex.sharedDataGcLock.Lock() cc.offline = offlinePreviousState cc.cacheMutex.sharedDataGcLock.Unlock() if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"task": "UpdateCache", "clientMode": cc.clientMode}, LogError, err.Error()) } return } // Signal that the cache build is done <-resultsDone if cc.logFn != nil && cc.logLevel <= LogInfo { cc.logFn(map[string]interface{}{"time elapsed": fmt.Sprint(time.Since(start)), "task": "UpdateCache", "clientMode": cc.clientMode}, LogInfo, InfoUpdateCacheTime) } cc.cacheMutex.fullCacheGcLock.Lock() cc.cacheMutex.sharedDataGcLock.Lock() cc.cacheMutex.assetsGcLock.Lock() cc.cacheMutex.idContentTypeMapGcLock.Lock() cc.cacheMutex.parentMapGcLock.Lock() cc.cacheMutex.genericEntriesGcLock.Lock() cc.cacheMutex.brandGcLock.Lock() cc.cacheMutex.categoryGcLock.Lock() cc.cacheMutex.productGcLock.Lock() cc.Cache = tempCache cc.offline = offlinePreviousState cc.cacheMutex.brandGcLock.Unlock() cc.cacheMutex.categoryGcLock.Unlock() cc.cacheMutex.productGcLock.Unlock() cc.cacheMutex.parentMapGcLock.Unlock() cc.cacheMutex.idContentTypeMapGcLock.Unlock() cc.cacheMutex.assetsGcLock.Unlock() cc.cacheMutex.sharedDataGcLock.Unlock() cc.cacheMutex.fullCacheGcLock.Unlock() cc.cacheMutex.genericEntriesGcLock.Unlock() } func ToAssetReference(asset *contentful.Asset) (refSys ContentTypeSys) { refSys.Sys.ID = asset.Sys.ID refSys.Sys.Type = FieldTypeLink refSys.Sys.LinkType = FieldLinkTypeAsset return } func (cc *ContentfulClient) UpdateCacheForEntity(ctx context.Context, sysType string, contentType string, entityID string) error { if sysType == sysTypeEntry && cc.entryMapForContentTypeIsNil(contentType) { return fmt.Errorf("UpdateCacheForEntity: Content Type %q not available in cache", contentType) } if sysType == sysTypeAsset { contentType = assetWorkerType } if contentType != assetWorkerType && !stringSliceContains(spaceContentTypes, contentType) { return fmt.Errorf("UpdateCache: Content Type %q not available in this space", contentType) } return updateCacheForContentTypeAndEntity(ctx, cc, contentType, entityID, nil, false) } func FieldToObject(jsonField interface{}, targetObject interface{}) error { byteArray, err := json.Marshal(jsonField) if err != nil { return err } err = json.Unmarshal(byteArray, &targetObject) if err != nil { return err } return nil } func (cc *ContentfulClient) cacheGcAssetByID(ctx context.Context, id string, asset *contentful.Asset) error { if asset == nil { if cc.Client == nil { return errors.New("cacheGcAssetByID: No client available") } col := cc.Client.Assets.List(ctx, cc.SpaceID) col.Query.Locale("*").Equal("sys.id", id) _, err := col.Next() if err != nil { return err } if len(col.Items) == 0 { return errors.New("cacheGcAssetByID: Not found " + id) } item := col.Items[0] byt, err := json.Marshal(item) if err != nil { return err } err = json.Unmarshal(byt, &asset) if err != nil { return err } } for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} { if _, ok := asset.Fields.File[string(loc)]; ok { asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL } } cc.cacheMutex.assetsGcLock.Lock() cc.Cache.assets[id] = asset cc.cacheMutex.assetsGcLock.Unlock() return nil } func (cc *ContentfulClient) deleteAssetFromCache(key string) error { if cc.Cache == nil || cc.cacheMutex == nil { return errors.New("no cache available") } cc.cacheMutex.assetsGcLock.Lock() if _, ok := cc.Cache.assets[key]; ok { delete(cc.Cache.assets, key) cc.cacheMutex.assetsGcLock.Unlock() return nil } cc.cacheMutex.assetsGcLock.Unlock() return errors.New("asset not found in cache, could not delete") } func (cc *ContentfulClient) entryMapForContentTypeIsNil(contentType string) bool { switch contentType { case ContentTypeBrand: if cc.Cache.entryMaps.brand == nil { return true } case ContentTypeCategory: if cc.Cache.entryMaps.category == nil { return true } case ContentTypeProduct: if cc.Cache.entryMaps.product == nil { return true } } return false } func getContentfulAPIClient(clientMode ClientMode, clientKey string) (*contentful.Contentful, error) { switch clientMode { case ClientModeCDA: return contentful.NewCDA(clientKey), nil case ClientModeCPA: return contentful.NewCPA(clientKey), nil case ClientModeCMA: return contentful.NewCMA(clientKey), nil default: return nil, errors.New("NewContentfulClient: Unknown ClientMode") } } func (cc *ContentfulClient) getAllAssets(ctx context.Context, tryCacheFirst bool) (map[string]*contentful.Asset, error) { if cc == nil || cc.Client == nil { return nil, errors.New("getAllAssets: No client available") } cc.cacheMutex.sharedDataGcLock.RLock() offline := cc.offline cacheInit := cc.cacheInit cc.cacheMutex.sharedDataGcLock.RUnlock() if cacheInit && cc.Cache.assets != nil && tryCacheFirst { return cc.Cache.assets, nil } allItems := []interface{}{} assets := map[string]*contentful.Asset{} if offline { for _, asset := range cc.offlineTemp.Assets { allItems = append(allItems, asset) } } else { col := cc.Client.Assets.List(ctx, cc.SpaceID) col.Query.Locale("*").Limit(assetPageSize) for { _, err := col.Next() if err != nil { return nil, err } allItems = append(allItems, col.Items...) if uint16(len(col.Items)) < assetPageSize { break } } } for _, item := range allItems { asset := contentful.Asset{} byt, err := json.Marshal(item) if err != nil { return nil, err } err = json.Unmarshal(byt, &asset) if err != nil { return nil, err } for _, loc := range []Locale{SpaceLocaleGerman, SpaceLocaleFrench} { if _, ok := asset.Fields.File[string(loc)]; ok { asset.Fields.File[string(loc)].URL = "https:" + asset.Fields.File[string(loc)].URL } } assets[asset.Sys.ID] = &asset } return assets, nil } func (cc *ContentfulClient) getAllTags(ctx context.Context, tryCacheFirst bool) (map[string]string, error) { if cc == nil || cc.Client == nil { return nil, errors.New("getAllTags: No client available") } cc.cacheMutex.sharedDataGcLock.RLock() offline := cc.offline cacheInit := cc.cacheInit cc.cacheMutex.sharedDataGcLock.RUnlock() cc.cacheMutex.tagGcLock.RLock() defer cc.cacheMutex.tagGcLock.RUnlock() if cacheInit && cc.Cache.tags != nil && tryCacheFirst { return cc.Cache.tags, nil } allItems := []interface{}{} tags := map[string]string{} if offline { for _, asset := range cc.offlineTemp.Tags { allItems = append(allItems, asset) } } else { col := cc.Client.Tags.List(ctx, cc.SpaceID) col.Query.Limit(1000) _, err := col.Next() if err != nil { return nil, err } allItems = col.Items } for _, item := range allItems { tag := contentful.Tag{} byt, err := json.Marshal(item) if err != nil { return nil, err } err = json.Unmarshal(byt, &tag) if err != nil { return nil, err } tags[tag.Name] = tag.Sys.ID } return tags, nil } func getOfflineSpaceFromFile(file []byte) (*offlineTemp, error) { offlineTemp := &offlineTemp{} err := json.Unmarshal(file, offlineTemp) if err != nil { return nil, fmt.Errorf("getOfflineSpaceFromFile could not parse space export file: %v", err) } return offlineTemp, nil } func (cc *ContentfulClient) optimisticPageSizeGetAll(ctx context.Context, contentType string, limit uint16) (*contentful.Collection, error) { col := cc.Client.Entries.List(ctx, cc.SpaceID) col.Query.ContentType(contentType).Locale("*").Include(0).Limit(limit) allItems := []interface{}{} var err error for { _, err = col.Next() if err != nil { break } allItems = append(allItems, col.Items...) if uint16(len(col.Items)) < limit { break } } col.Items = allItems switch errTyped := err.(type) { case contentful.ErrorResponse: msg := errTyped.Message if (strings.Contains(msg, "Response size too big") || strings.Contains(msg, "Too many links")) && limit >= 20 { smallerPageCol, err := cc.optimisticPageSizeGetAll(ctx, contentType, limit/2) return smallerPageCol, err } return nil, err case nil: default: return nil, err } return col, nil } func richTextGetAttribute(htmlLine string, attribute string) string { re := regexp.MustCompile(` href=["']([^"']+)["']`) matches := re.FindStringSubmatch(htmlLine) if len(matches) == 2 { return strings.ToLower(matches[1]) } return "" } func richTextGetMark(mark string) string { switch mark { case HtmlItalic, HtmlEm: return RichTextMarkItalic case HtmlBold, HtmlStrong: return RichTextMarkBold case HtmlUnderline: return RichTextMarkUnderline case HtmlCode: return RichTextMarkCode } return "" } func richTextHtmlLinesToNode(htmlLines []string, start int, pendingTag string, marks []string, isBasic bool) ([]interface{}, int, bool) { nodeSlice := make([]interface{}, 0) for i := start; i < len(htmlLines); i++ { htmlLine := htmlLines[i] if richTextIsHtmlClosingPending(htmlLine, pendingTag) { return nodeSlice, i, isBasic } currentNode := RichTextNode{} if richTextIsHtmlTag(htmlLine) { tt := richTextHtmlTagType(htmlLine) switch tt { case HtmlParagraph, HtmlHeading1, HtmlHeading2, HtmlHeading3, HtmlHeading4, HtmlHeading5, HtmlHeading6, HtmlUnorderedList, HtmlOrderedList: isBasic = false currentNode.NodeType = richTextMapTagNodeType(tt) var nextLine int currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic) nodeSlice = append(nodeSlice, currentNode) if nextLine == -1 { return nodeSlice, -1, isBasic } i = nextLine case HtmlBlockquote, HtmlListItem: isBasic = false currentNode.NodeType = richTextMapTagNodeType(tt) currentNode.Content = make([]interface{}, 0) innerContent, nextLine, isBasic := richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic) if len(innerContent) == 1 { switch innerContent[0].(type) { case RichTextNodeTextNode: currentNode.Content = append(currentNode.Content, RichTextNode{ NodeType: RichTextNodeParagraph, Content: innerContent, }) } } else { for _, ct := range innerContent { switch ct.(type) { case RichTextNode: currentNode.Content = append(currentNode.Content, ct) case RichTextNodeTextNode: currentNode.Content = append(currentNode.Content, RichTextNode{ NodeType: RichTextNodeParagraph, Content: []interface{}{ct}, }) } } } nodeSlice = append(nodeSlice, currentNode) if nextLine == -1 { return nodeSlice, -1, isBasic } i = nextLine case HtmlCode: isBasic = false currentNode.NodeType = RichTextNodeParagraph var nextLine int currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, HtmlCode, []string{HtmlCode}, isBasic) nodeSlice = append(nodeSlice, currentNode) if nextLine == -1 { return nodeSlice, -1, isBasic } i = nextLine case HtmlAnchor: isBasic = false currentNode.NodeType = RichTextNodeHyperlink var nextLine int anchorURI := richTextGetAttribute(htmlLine, HtmlAttributeHref) if anchorURI == "" { anchorURI = "/" } currentNode.Data = RichTextData{URI: anchorURI} currentNode.Content, nextLine, isBasic = richTextHtmlLinesToNode(htmlLines, i+1, tt, marks, isBasic) nodeSlice = append(nodeSlice, currentNode) if nextLine == -1 { return nodeSlice, -1, isBasic } i = nextLine case HtmlHorizontalRule: isBasic = false currentNode.NodeType = RichTextNodeHR currentNode.Content = make([]interface{}, 0) nodeSlice = append(nodeSlice, currentNode) case HtmlItalic, HtmlEm, HtmlBold, HtmlStrong, HtmlUnderline: marks = append(marks, tt) case HtmlBreak: if len(nodeSlice) > 0 { myNode := nodeSlice[len(nodeSlice)-1] switch myNodeTyped := myNode.(type) { case RichTextNodeTextNode: if myNodeTyped.NodeType == RichTextNodeText { myNodeTyped.Value += "\n" nodeSlice[len(nodeSlice)-1] = myNodeTyped } } } } continue //unsupported tags will be ignored but content is preserved } if richTextIsHtmlClosingTag(htmlLine) { continue //closing tags that are not pending equal to unknown } if htmlLine == " " { continue } currentNodeTextNode := RichTextNodeTextNode{} currentNodeTextNode.NodeType = RichTextNodeText currentNodeTextNode.Marks = []RichTextMark{} currentNodeTextNode.Value = htmlLine for _, mark := range marks { currentNodeTextNode.Marks = append(currentNodeTextNode.Marks, RichTextMark{ Type: richTextGetMark(mark), }) } marks = nil if pendingTag == "" { nodeSlice = append(nodeSlice, RichTextNode{ NodeType: RichTextNodeParagraph, Content: []interface{}{currentNodeTextNode}, }) } else { nodeSlice = append(nodeSlice, currentNodeTextNode) } } return nodeSlice, -1, isBasic } func richTextIsHtmlClosingPending(htmlLine, tag string) bool { return regexp.MustCompile(`` + strings.ToLower(tag) + `.+$`).MatchString(strings.ToLower(htmlLine)) } func richTextIsHtmlClosingTag(htmlLine string) bool { return regexp.MustCompile(`[a-zA-Z]+.+$`).MatchString(htmlLine) } func richTextIsHtmlTag(htmlLine string) bool { return regexp.MustCompile(`<[a-zA-Z]+.+$`).MatchString(htmlLine) } func richTextMapTagNodeType(tag string) string { switch tag { case HtmlParagraph: return RichTextNodeParagraph case HtmlAnchor: return RichTextNodeHyperlink case HtmlHeading1: return RichTextNodeHeading1 case HtmlHeading2: return RichTextNodeHeading2 case HtmlHeading3: return RichTextNodeHeading3 case HtmlHeading4: return RichTextNodeHeading4 case HtmlHeading5: return RichTextNodeHeading5 case HtmlHeading6: return RichTextNodeHeading6 case HtmlBlockquote: return RichTextNodeBlockquote case HtmlUnorderedList: return RichTextNodeUnorderedList case HtmlOrderedList: return RichTextNodeOrderedList case HtmlListItem: return RichTextNodeListItem } return "" } func (ts richTextHtmlTags) richTextHtmlTagsOpen(w io.Writer) { for _, t := range ts { if t.customHTML != "" { w.Write([]byte(t.customHTML)) continue } tagString := "<" + t.name if len(t.attrs) > 0 { for name, value := range t.attrs { tagString += " " + name + `="` + html.EscapeString(value) + `"` } } w.Write([]byte(tagString + ">")) } } func (ts richTextHtmlTags) richTextHtmlTagsClose(w io.Writer) { for _, t := range ts { if t.customHTML != "" { continue } w.Write([]byte("" + t.name + ">")) } } func richTextHtmlTagType(htmlLine string) string { re := regexp.MustCompile(`^<([a-zA-Z0-9]+)[\s]*>*`) matches := re.FindStringSubmatch(htmlLine) if len(matches) == 2 { return strings.ToLower(matches[1]) } return "" } func (n *RichTextGenericNode) richTextRenderHTML(w io.Writer, linkResolver LinkResolverFunc, entryLinkResolver EntryLinkResolverFunc, imageResolver ImageResolverFunc, embeddedEntryResolver EmbeddedEntryResolverFunc, locale Locale) (err error) { if linkResolver == nil { linkResolver = func(url string) (transformedAttrs map[string]string, err error) { return map[string]string{ "href": url, }, nil } } if entryLinkResolver == nil { entryLinkResolver = func(entryID string, locale Locale) (transformedAttrs map[string]string, err error) { return map[string]string{}, nil } } if embeddedEntryResolver == nil { embeddedEntryResolver = func(entryID string, locale Locale) (htmlSnippet string, err error) { return "", nil } } if imageResolver == nil { imageResolver = func(assetID string, locale Locale) (attrs map[string]string, customHTML string, resolveError error) { return map[string]string{}, "", nil } } tags := richTextHtmlTags{} switch n.NodeType { case RichTextNodeParagraph: tags = []richTextHtmlTag{{name: HtmlParagraph}} case RichTextNodeHeading1: tags = []richTextHtmlTag{{name: HtmlHeading1}} case RichTextNodeHeading2: tags = []richTextHtmlTag{{name: HtmlHeading2}} case RichTextNodeHeading3: tags = []richTextHtmlTag{{name: HtmlHeading3}} case RichTextNodeHeading4: tags = []richTextHtmlTag{{name: HtmlHeading4}} case RichTextNodeHeading5: tags = []richTextHtmlTag{{name: HtmlHeading5}} case RichTextNodeHeading6: tags = []richTextHtmlTag{{name: HtmlHeading6}} case RichTextNodeOrderedList: tags = []richTextHtmlTag{{name: HtmlOrderedList}} case RichTextNodeUnorderedList: tags = []richTextHtmlTag{{name: HtmlUnorderedList}} case RichTextNodeListItem: tags = []richTextHtmlTag{{name: HtmlListItem}} case RichTextNodeHR: tags = []richTextHtmlTag{{name: HtmlHorizontalRule}} case RichTextNodeBlockquote: tags = []richTextHtmlTag{{name: HtmlBlockquote}} case RichTextNodeText: tags = []richTextHtmlTag{} case RichTextNodeHyperlink: if n.Data != nil { uri := n.Data["uri"] attrs := map[string]string{} switch uriString := uri.(type) { case string: if uriString != "" { resolvedAttrs, errResolveAttrs := linkResolver(uriString) if errResolveAttrs != nil { err = errResolveAttrs return } attrs = resolvedAttrs } } tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlAnchor, attrs: attrs}} } case RichTextNodeEntryHyperlink: if n.Data != nil { target := n.Data["target"] attrs := map[string]string{} switch target.(type) { case map[string]interface{}: targetSys, ok := target.(map[string]interface{})["sys"] if ok { entryID := targetSys.(map[string]interface{})["id"].(string) resolvedAttrs, errResolveAttrs := entryLinkResolver(entryID, locale) if errResolveAttrs != nil { err = errResolveAttrs return } attrs = resolvedAttrs } } tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlAnchor, attrs: attrs}} } case RichTextNodeEmbeddedAsset: if imageResolver == nil { return errors.New("can't resolve image asset URL") } dataObj := RichTextData{} byt, err := json.Marshal(n.Data) if err != nil { return err } err = json.Unmarshal(byt, &dataObj) if err != nil { return err } if dataObj.Target == nil { return errors.New("data target is empty") } attrs, customHTML, err := imageResolver(dataObj.Target.Sys.ID, locale) if err != nil { return err } tags = []richTextHtmlTag{richTextHtmlTag{name: HtmlImage, attrs: attrs, customHTML: customHTML}} case RichTextNodeEmbeddedEntry: dataObj := RichTextData{} byt, errMarshal := json.Marshal(n.Data) if errMarshal != nil { return errMarshal } errUnmarshal := json.Unmarshal(byt, &dataObj) if errUnmarshal != nil { return errUnmarshal } if dataObj.Target == nil { return errors.New("data target is empty") } rawHTML, errResolve := embeddedEntryResolver(dataObj.Target.Sys.ID, locale) if errResolve != nil { return errResolve } w.Write([]byte(rawHTML)) return case RichTextNodeTable: tags = []richTextHtmlTag{{name: HtmlTable}} case RichTextNodeTableRow: tags = []richTextHtmlTag{{name: HtmlTableRow}} case RichTextNodeTableHeaderCell: tags = []richTextHtmlTag{{name: HtmlTableHeaderCell}} case RichTextNodeTableCell: tags = []richTextHtmlTag{{name: HtmlTableCell}} default: } for _, m := range n.Marks { markTag := "" switch m.Type { case RichTextMarkBold: markTag = HtmlBold case RichTextMarkItalic: markTag = HtmlItalic case RichTextMarkUnderline: markTag = HtmlUnderline case RichTextMarkCode: markTag = HtmlCode case RichTextNodeBlockquote: markTag = HtmlBlockquote } if markTag != "" { tags = append(tags, richTextHtmlTag{name: markTag}) } } tags.richTextHtmlTagsOpen(w) cleanString := strings.Replace(html.EscapeString(n.Value), "\n", "