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