Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

387 рядки
11KB

  1. package mapping
  2. import (
  3. "fmt"
  4. "math"
  5. "regexp"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. "qctextbuilder/internal/domain"
  10. )
  11. type SemanticSlotTarget struct {
  12. Slot string `json:"slot"`
  13. FieldPath string `json:"fieldPath"`
  14. FieldKey string `json:"fieldKey"`
  15. DisplayLabel string `json:"displayLabel,omitempty"`
  16. WebsiteSection string `json:"websiteSection,omitempty"`
  17. BlockID string `json:"blockId,omitempty"`
  18. Reason string `json:"reason,omitempty"`
  19. }
  20. type SemanticSlotMapping struct {
  21. Targets []SemanticSlotTarget `json:"targets"`
  22. BySlot map[string][]SemanticSlotTarget
  23. }
  24. var semanticBlockIDPattern = regexp.MustCompile(`(?i)(?:^|[_.])([mcr]\d{3,})(?:[_.]|$)`)
  25. var semanticLooseBlockIDPattern = regexp.MustCompile(`(?i)([mcr]\d{3,})`)
  26. var semanticIndexSuffixPattern = regexp.MustCompile(`_\d+$`)
  27. var semanticTrailingNumberPattern = regexp.MustCompile(`_(\d+)$`)
  28. func MapTemplateFieldsToSemanticSlots(fields []domain.TemplateField) SemanticSlotMapping {
  29. sectionGroupIndex := map[string]map[string]int{
  30. domain.WebsiteSectionServices: {},
  31. domain.WebsiteSectionServiceItem: {},
  32. domain.WebsiteSectionTeam: {},
  33. domain.WebsiteSectionTestimonials: {},
  34. }
  35. sectionGroupNext := map[string]int{
  36. domain.WebsiteSectionServices: 0,
  37. domain.WebsiteSectionServiceItem: 0,
  38. domain.WebsiteSectionTeam: 0,
  39. domain.WebsiteSectionTestimonials: 0,
  40. }
  41. repeatedIndexResolver := newSemanticRepeatedIndexResolver(fields)
  42. targets := make([]SemanticSlotTarget, 0)
  43. for _, field := range fields {
  44. if !field.IsEnabled || !strings.EqualFold(strings.TrimSpace(field.FieldKind), "text") {
  45. continue
  46. }
  47. section := semanticSection(field)
  48. role := semanticRole(field)
  49. slot, mapped := semanticSlotForField(field, section, role, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  50. if !mapped {
  51. continue
  52. }
  53. reason := "section=" + section + ", role=" + role
  54. if blockID := semanticExtractBlockID(field); blockID != "" {
  55. reason += ", block=" + blockID
  56. }
  57. targets = append(targets, SemanticSlotTarget{
  58. Slot: slot,
  59. FieldPath: strings.TrimSpace(field.Path),
  60. FieldKey: strings.TrimSpace(field.KeyName),
  61. DisplayLabel: strings.TrimSpace(field.DisplayLabel),
  62. WebsiteSection: section,
  63. BlockID: semanticExtractBlockID(field),
  64. Reason: reason,
  65. })
  66. }
  67. sort.SliceStable(targets, func(i, j int) bool {
  68. if targets[i].Slot == targets[j].Slot {
  69. return targets[i].FieldPath < targets[j].FieldPath
  70. }
  71. return targets[i].Slot < targets[j].Slot
  72. })
  73. bySlot := make(map[string][]SemanticSlotTarget, len(targets))
  74. for _, target := range targets {
  75. bySlot[target.Slot] = append(bySlot[target.Slot], target)
  76. }
  77. return SemanticSlotMapping{
  78. Targets: targets,
  79. BySlot: bySlot,
  80. }
  81. }
  82. func semanticSlotForField(
  83. field domain.TemplateField,
  84. section string,
  85. role string,
  86. sectionGroupIndex map[string]map[string]int,
  87. sectionGroupNext map[string]int,
  88. repeatedIndexResolver *semanticRepeatedIndexResolver,
  89. ) (string, bool) {
  90. switch section {
  91. case domain.WebsiteSectionHero:
  92. if role == "title" {
  93. return "hero.title", true
  94. }
  95. case domain.WebsiteSectionIntro:
  96. if role == "title" {
  97. return "intro.title", true
  98. }
  99. if role == "description" {
  100. return "intro.description", true
  101. }
  102. case domain.WebsiteSectionAbout:
  103. if role == "description" || role == "title" {
  104. return "about.description", true
  105. }
  106. case domain.WebsiteSectionServices, domain.WebsiteSectionServiceItem:
  107. if role == "title" || role == "description" {
  108. index := semanticRepeatedIndex(section, role, field, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  109. return fmt.Sprintf("service_items[%d].%s", index, role), true
  110. }
  111. case domain.WebsiteSectionTeam:
  112. if role == "name" || role == "description" {
  113. index := semanticRepeatedIndex(section, role, field, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  114. return fmt.Sprintf("team_items[%d].%s", index, role), true
  115. }
  116. case domain.WebsiteSectionTestimonials:
  117. if role == "title" || role == "description" || role == "name" {
  118. index := semanticRepeatedIndex(section, role, field, sectionGroupIndex, sectionGroupNext, repeatedIndexResolver)
  119. return fmt.Sprintf("testimonial_items[%d].%s", index, role), true
  120. }
  121. case domain.WebsiteSectionCTA:
  122. if role == "cta_text" || role == "title" || role == "description" {
  123. return "cta.text", true
  124. }
  125. }
  126. return "", false
  127. }
  128. func semanticRepeatedIndex(
  129. section string,
  130. role string,
  131. field domain.TemplateField,
  132. sectionGroupIndex map[string]map[string]int,
  133. sectionGroupNext map[string]int,
  134. repeatedIndexResolver *semanticRepeatedIndexResolver,
  135. ) int {
  136. if repeatedIndexResolver != nil {
  137. if idx, ok := repeatedIndexResolver.IndexFor(section, role, field); ok {
  138. return idx
  139. }
  140. }
  141. return semanticGroupIndex(section, field, sectionGroupIndex, sectionGroupNext)
  142. }
  143. func semanticGroupIndex(
  144. section string,
  145. field domain.TemplateField,
  146. sectionGroupIndex map[string]map[string]int,
  147. sectionGroupNext map[string]int,
  148. ) int {
  149. normalizedSection := domain.NormalizeWebsiteSection(section)
  150. group := semanticGroupKey(field)
  151. if _, ok := sectionGroupIndex[normalizedSection]; !ok {
  152. sectionGroupIndex[normalizedSection] = map[string]int{}
  153. }
  154. if idx, ok := sectionGroupIndex[normalizedSection][group]; ok {
  155. return idx
  156. }
  157. idx := sectionGroupNext[normalizedSection]
  158. sectionGroupNext[normalizedSection] = idx + 1
  159. sectionGroupIndex[normalizedSection][group] = idx
  160. return idx
  161. }
  162. func semanticGroupKey(field domain.TemplateField) string {
  163. if blockID := semanticExtractBlockID(field); blockID != "" {
  164. return "block:" + blockID
  165. }
  166. key := strings.ToLower(strings.TrimSpace(field.KeyName))
  167. if key != "" {
  168. return "key:" + semanticIndexSuffixPattern.ReplaceAllString(key, "")
  169. }
  170. path := strings.ToLower(strings.TrimSpace(field.Path))
  171. return "path:" + semanticIndexSuffixPattern.ReplaceAllString(path, "")
  172. }
  173. func semanticSection(field domain.TemplateField) string {
  174. websiteSection := domain.NormalizeWebsiteSection(field.WebsiteSection)
  175. if websiteSection != domain.WebsiteSectionOther {
  176. return websiteSection
  177. }
  178. return domain.SuggestWebsiteSection(field)
  179. }
  180. func semanticRole(field domain.TemplateField) string {
  181. parts := []string{
  182. strings.ToLower(strings.TrimSpace(field.KeyName)),
  183. strings.ToLower(strings.TrimSpace(field.Path)),
  184. strings.ToLower(strings.TrimSpace(field.DisplayLabel)),
  185. strings.ToLower(strings.TrimSpace(field.Section)),
  186. }
  187. combined := strings.Join(parts, " ")
  188. switch {
  189. case semanticContainsAny(combined, "description", "subtitle", "paragraph", "copy", "body", "content", "mission", "story", "bio", "quote"):
  190. return "description"
  191. case semanticContainsAny(combined, "button", "btn", "calltoaction", "call_to_action", "cta"):
  192. return "cta_text"
  193. case semanticContainsAny(combined, "headline", "heading", "title"):
  194. return "title"
  195. case semanticContainsAny(combined, "author", "customer", "person", "member", "name"):
  196. return "name"
  197. default:
  198. return "description"
  199. }
  200. }
  201. func semanticExtractBlockID(field domain.TemplateField) string {
  202. candidates := []string{
  203. strings.TrimSpace(field.KeyName),
  204. strings.TrimSpace(field.Path),
  205. strings.TrimSpace(field.DisplayLabel),
  206. }
  207. for _, candidate := range candidates {
  208. if candidate == "" {
  209. continue
  210. }
  211. if match := semanticBlockIDPattern.FindStringSubmatch(candidate); len(match) > 1 {
  212. return strings.ToLower(match[1])
  213. }
  214. }
  215. for _, candidate := range candidates {
  216. if candidate == "" {
  217. continue
  218. }
  219. if match := semanticLooseBlockIDPattern.FindStringSubmatch(candidate); len(match) > 1 {
  220. return strings.ToLower(match[1])
  221. }
  222. }
  223. return ""
  224. }
  225. func semanticContainsAny(value string, needles ...string) bool {
  226. for _, needle := range needles {
  227. if strings.Contains(value, needle) {
  228. return true
  229. }
  230. }
  231. return false
  232. }
  233. type semanticRepeatedIndexResolver struct {
  234. byFieldKey map[string]int
  235. }
  236. type semanticRepeatedField struct {
  237. fieldKey string
  238. suffix int
  239. path string
  240. }
  241. func newSemanticRepeatedIndexResolver(fields []domain.TemplateField) *semanticRepeatedIndexResolver {
  242. resolver := &semanticRepeatedIndexResolver{
  243. byFieldKey: map[string]int{},
  244. }
  245. // Pair repeated fields by section + block + role + numeric suffix ordering.
  246. buckets := map[string][]semanticRepeatedField{}
  247. for _, field := range fields {
  248. if !field.IsEnabled || !strings.EqualFold(strings.TrimSpace(field.FieldKind), "text") {
  249. continue
  250. }
  251. section := semanticSection(field)
  252. if !semanticIsRepeatedSection(section) {
  253. continue
  254. }
  255. role := semanticRole(field)
  256. if !semanticRoleAllowedForRepeated(section, role) {
  257. continue
  258. }
  259. suffix, ok := semanticTrailingNumber(field)
  260. if !ok {
  261. continue
  262. }
  263. bucket := semanticRepeatedBucketKey(section, semanticExtractBlockID(field), role)
  264. entry := semanticRepeatedField{
  265. fieldKey: semanticFieldIdentity(field),
  266. suffix: suffix,
  267. path: strings.TrimSpace(field.Path),
  268. }
  269. buckets[bucket] = append(buckets[bucket], entry)
  270. }
  271. for _, bucketEntries := range buckets {
  272. sort.SliceStable(bucketEntries, func(i, j int) bool {
  273. if bucketEntries[i].suffix != bucketEntries[j].suffix {
  274. return bucketEntries[i].suffix < bucketEntries[j].suffix
  275. }
  276. return bucketEntries[i].path < bucketEntries[j].path
  277. })
  278. for idx, entry := range bucketEntries {
  279. resolver.byFieldKey[entry.fieldKey] = idx
  280. }
  281. }
  282. return resolver
  283. }
  284. func (r *semanticRepeatedIndexResolver) IndexFor(section string, role string, field domain.TemplateField) (int, bool) {
  285. if r == nil || len(r.byFieldKey) == 0 {
  286. return 0, false
  287. }
  288. if !semanticIsRepeatedSection(section) || !semanticRoleAllowedForRepeated(section, role) {
  289. return 0, false
  290. }
  291. idx, ok := r.byFieldKey[semanticFieldIdentity(field)]
  292. return idx, ok
  293. }
  294. func semanticIsRepeatedSection(section string) bool {
  295. switch domain.NormalizeWebsiteSection(section) {
  296. case domain.WebsiteSectionServices, domain.WebsiteSectionServiceItem, domain.WebsiteSectionTeam, domain.WebsiteSectionTestimonials:
  297. return true
  298. default:
  299. return false
  300. }
  301. }
  302. func semanticRoleAllowedForRepeated(section string, role string) bool {
  303. switch domain.NormalizeWebsiteSection(section) {
  304. case domain.WebsiteSectionServices, domain.WebsiteSectionServiceItem:
  305. return role == "title" || role == "description"
  306. case domain.WebsiteSectionTeam:
  307. return role == "name" || role == "description"
  308. case domain.WebsiteSectionTestimonials:
  309. return role == "name" || role == "title" || role == "description"
  310. default:
  311. return false
  312. }
  313. }
  314. func semanticRepeatedBucketKey(section string, blockID string, role string) string {
  315. normalizedSection := domain.NormalizeWebsiteSection(section)
  316. if normalizedSection == domain.WebsiteSectionServiceItem {
  317. normalizedSection = domain.WebsiteSectionServices
  318. }
  319. block := strings.TrimSpace(strings.ToLower(blockID))
  320. if block == "" {
  321. block = "__no_block__"
  322. }
  323. return normalizedSection + "|" + block + "|" + role
  324. }
  325. func semanticTrailingNumber(field domain.TemplateField) (int, bool) {
  326. candidates := []string{
  327. strings.TrimSpace(strings.ToLower(field.Path)),
  328. strings.TrimSpace(strings.ToLower(field.KeyName)),
  329. }
  330. best := math.MaxInt
  331. found := false
  332. for _, candidate := range candidates {
  333. if candidate == "" {
  334. continue
  335. }
  336. match := semanticTrailingNumberPattern.FindStringSubmatch(candidate)
  337. if len(match) < 2 {
  338. continue
  339. }
  340. value, err := strconv.Atoi(match[1])
  341. if err != nil {
  342. continue
  343. }
  344. if value < best {
  345. best = value
  346. }
  347. found = true
  348. }
  349. if !found {
  350. return 0, false
  351. }
  352. return best, true
  353. }
  354. func semanticFieldIdentity(field domain.TemplateField) string {
  355. return strings.ToLower(strings.TrimSpace(field.Path)) + "|" + strings.ToLower(strings.TrimSpace(field.KeyName))
  356. }