blob: 6ed425c6678e932031bc05dfcdf60a9c0e6efc0e [file] [log] [blame]
Akrona3675e92025-06-26 17:46:59 +02001package mapper
2
3import (
4 "fmt"
5 "strings"
6
7 "github.com/KorAP/KoralPipe-TermMapper/ast"
8 "github.com/KorAP/KoralPipe-TermMapper/matcher"
Akrona1337ef2025-07-01 12:28:03 +02009 "github.com/rs/zerolog/log"
Akrona3675e92025-06-26 17:46:59 +020010)
11
12// ApplyResponseMappings applies the specified mapping rules to a JSON object
13func (m *Mapper) ApplyResponseMappings(mappingID string, opts MappingOptions, jsonData any) (any, error) {
14 // Validate mapping ID
15 if _, exists := m.mappingLists[mappingID]; !exists {
16 return nil, fmt.Errorf("mapping list with ID %s not found", mappingID)
17 }
18
19 // Get the parsed rules
20 rules := m.parsedRules[mappingID]
21
22 // Check if we have a snippet to process
23 jsonMap, ok := jsonData.(map[string]any)
24 if !ok {
25 return jsonData, nil
26 }
27
28 snippetValue, exists := jsonMap["snippet"]
29 if !exists {
30 return jsonData, nil
31 }
32
33 snippet, ok := snippetValue.(string)
34 if !ok {
35 return jsonData, nil
36 }
37
38 // Process the snippet with each rule
39 processedSnippet := snippet
40 for _, rule := range rules {
41 // Create pattern and replacement based on direction
42 var pattern, replacement ast.Node
43 if opts.Direction { // true means AtoB
44 pattern = rule.Upper
45 replacement = rule.Lower
46 } else {
47 pattern = rule.Lower
48 replacement = rule.Upper
49 }
50
51 // Extract the inner nodes from the pattern and replacement tokens
52 if token, ok := pattern.(*ast.Token); ok {
53 pattern = token.Wrap
54 }
55 if token, ok := replacement.(*ast.Token); ok {
56 replacement = token.Wrap
57 }
58
59 // Apply foundry and layer overrides to pattern and replacement
60 var patternFoundry, patternLayer, replacementFoundry, replacementLayer string
61 if opts.Direction { // true means AtoB
62 patternFoundry, patternLayer = opts.FoundryA, opts.LayerA
63 replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB
64 } else {
65 patternFoundry, patternLayer = opts.FoundryB, opts.LayerB
66 replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA
67 }
68
69 // If foundry/layer are empty in options, get them from the mapping list
Akron4de47a92025-06-27 11:58:11 +020070 mappingList := m.mappingLists[mappingID]
71 if replacementFoundry == "" {
Akrona3675e92025-06-26 17:46:59 +020072 if opts.Direction { // AtoB
73 replacementFoundry = mappingList.FoundryB
Akrona3675e92025-06-26 17:46:59 +020074 } else {
75 replacementFoundry = mappingList.FoundryA
Akron4de47a92025-06-27 11:58:11 +020076 }
77 }
78 if replacementLayer == "" {
79 if opts.Direction { // AtoB
80 replacementLayer = mappingList.LayerB
81 } else {
Akrona3675e92025-06-26 17:46:59 +020082 replacementLayer = mappingList.LayerA
83 }
84 }
85
86 // Clone pattern and apply overrides
87 processedPattern := pattern.Clone()
88 if patternFoundry != "" || patternLayer != "" {
89 ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer)
90 }
91
Akrona3675e92025-06-26 17:46:59 +020092 // Create snippet matcher for this rule
93 snippetMatcher, err := matcher.NewSnippetMatcher(
94 ast.Pattern{Root: processedPattern},
95 ast.Replacement{Root: replacement},
96 )
97 if err != nil {
98 continue // Skip this rule if we can't create a matcher
99 }
100
101 // Find matching tokens in the snippet
102 matchingTokens, err := snippetMatcher.FindMatchingTokens(processedSnippet)
103 if err != nil {
104 continue // Skip this rule if parsing fails
105 }
106
107 if len(matchingTokens) == 0 {
108 continue // No matches, try next rule
109 }
110
111 // Apply RestrictToObligatory to the replacement to get the annotations to add
112 // Note: Only pass foundry override, not layer, since replacement terms have correct layers
113 restrictedReplacement := ast.RestrictToObligatory(replacement, replacementFoundry, "")
114 if restrictedReplacement == nil {
115 continue // Nothing obligatory to add
116 }
117
118 // Generate annotation strings from the restricted replacement
119 annotationStrings, err := m.generateAnnotationStrings(restrictedReplacement)
120 if err != nil {
121 continue // Skip if we can't generate annotations
122 }
123
124 if len(annotationStrings) == 0 {
125 continue // Nothing to add
126 }
127
128 // Apply annotations to matching tokens in the snippet
129 processedSnippet, err = m.addAnnotationsToSnippet(processedSnippet, matchingTokens, annotationStrings)
130 if err != nil {
131 continue // Skip if we can't apply annotations
132 }
133 }
134
Akrona1337ef2025-07-01 12:28:03 +0200135 log.Debug().Str("snippet", processedSnippet).Msg("Processed snippet")
136
Akrona3675e92025-06-26 17:46:59 +0200137 // Create a copy of the input data and update the snippet
138 result := make(map[string]any)
139 for k, v := range jsonMap {
140 result[k] = v
141 }
142 result["snippet"] = processedSnippet
143
144 return result, nil
145}
146
147// generateAnnotationStrings converts a replacement AST node into annotation strings
148func (m *Mapper) generateAnnotationStrings(node ast.Node) ([]string, error) {
149 if node == nil {
150 return nil, nil
151 }
152
153 switch n := node.(type) {
154 case *ast.Term:
155 // Create annotation string in format "foundry/layer:key" or "foundry/layer:key:value"
156 annotation := n.Foundry + "/" + n.Layer + ":" + n.Key
157 if n.Value != "" {
158 annotation += ":" + n.Value
159 }
160 return []string{annotation}, nil
161
162 case *ast.TermGroup:
163 if n.Relation == ast.AndRelation {
164 // For AND groups, collect all annotations
165 var allAnnotations []string
166 for _, operand := range n.Operands {
167 annotations, err := m.generateAnnotationStrings(operand)
168 if err != nil {
169 return nil, err
170 }
171 allAnnotations = append(allAnnotations, annotations...)
172 }
173 return allAnnotations, nil
174 } else {
175 // For OR groups (should not happen with RestrictToObligatory, but handle gracefully)
176 return nil, nil
177 }
178
179 case *ast.Token:
180 // Handle wrapped tokens
181 if n.Wrap != nil {
182 return m.generateAnnotationStrings(n.Wrap)
183 }
184 return nil, nil
185
186 default:
187 return nil, nil
188 }
189}
190
191// addAnnotationsToSnippet adds new annotations to matching tokens in the snippet
192func (m *Mapper) addAnnotationsToSnippet(snippet string, matchingTokens []matcher.TokenSpan, annotationStrings []string) (string, error) {
193 if len(matchingTokens) == 0 || len(annotationStrings) == 0 {
194 return snippet, nil
195 }
196
197 result := snippet
198
199 // Process each matching token
200 for _, token := range matchingTokens {
201 // For nested span structure, we need to find the innermost text and wrap it
202 // Look for the actual token text within span tags
203 tokenText := token.Text
204
205 // Find all occurrences of the token text in the current snippet
206 // We need to be careful about which occurrence to replace
207 startPos := 0
208 for {
209 tokenStart := strings.Index(result[startPos:], tokenText)
210 if tokenStart == -1 {
211 break // No more occurrences
212 }
213 tokenStart += startPos
214 tokenEnd := tokenStart + len(tokenText)
215
216 // Check if this token text is within the expected context
217 // Look backwards and forwards to see if we're in the right span context
218 beforeContext := result[:tokenStart]
219 afterContext := result[tokenEnd:]
220
221 // Simple heuristic: if we're immediately preceded by a > and followed by a <
222 // then we're likely at the innermost text node
223 if strings.HasSuffix(beforeContext, ">") && (strings.HasPrefix(afterContext, "<") || len(afterContext) == 0 || afterContext[0] == ' ') {
224 // Build the replacement with nested spans for each annotation
225 replacement := tokenText
226 for i := len(annotationStrings) - 1; i >= 0; i-- {
227 replacement = fmt.Sprintf(`<span title="%s" class="notinindex">%s</span>`, annotationStrings[i], replacement)
228 }
229
230 // Replace this occurrence
231 result = result[:tokenStart] + replacement + result[tokenEnd:]
232 break // Only replace the first appropriate occurrence for this token
233 }
234
235 // Move past this occurrence
236 startPos = tokenEnd
237 }
238 }
239
240 return result, nil
241}