Fix layer precedence and document precedences
Change-Id: I7fdc9f9122d7e723f98674a9f9e3010c38118ba9
diff --git a/mapper/response.go b/mapper/response.go
index 6ed425c..e89d12a 100644
--- a/mapper/response.go
+++ b/mapper/response.go
@@ -6,6 +6,7 @@
"github.com/KorAP/KoralPipe-TermMapper/ast"
"github.com/KorAP/KoralPipe-TermMapper/matcher"
+ "github.com/KorAP/KoralPipe-TermMapper/parser"
"github.com/rs/zerolog/log"
)
@@ -37,7 +38,7 @@
// Process the snippet with each rule
processedSnippet := snippet
- for _, rule := range rules {
+ for ruleIndex, rule := range rules {
// Create pattern and replacement based on direction
var pattern, replacement ast.Node
if opts.Direction { // true means AtoB
@@ -56,34 +57,34 @@
replacement = token.Wrap
}
- // Apply foundry and layer overrides to pattern and replacement
+ // Apply foundry and layer overrides with proper precedence
+ mappingList := m.mappingLists[mappingID]
+
+ // Determine foundry and layer values based on direction
var patternFoundry, patternLayer, replacementFoundry, replacementLayer string
- if opts.Direction { // true means AtoB
+ if opts.Direction { // AtoB
patternFoundry, patternLayer = opts.FoundryA, opts.LayerA
replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB
- } else {
+ // Apply mapping list defaults if not specified
+ if replacementFoundry == "" {
+ replacementFoundry = mappingList.FoundryB
+ }
+ if replacementLayer == "" {
+ replacementLayer = mappingList.LayerB
+ }
+ } else { // BtoA
patternFoundry, patternLayer = opts.FoundryB, opts.LayerB
replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA
- }
-
- // If foundry/layer are empty in options, get them from the mapping list
- mappingList := m.mappingLists[mappingID]
- if replacementFoundry == "" {
- if opts.Direction { // AtoB
- replacementFoundry = mappingList.FoundryB
- } else {
+ // Apply mapping list defaults if not specified
+ if replacementFoundry == "" {
replacementFoundry = mappingList.FoundryA
}
- }
- if replacementLayer == "" {
- if opts.Direction { // AtoB
- replacementLayer = mappingList.LayerB
- } else {
+ if replacementLayer == "" {
replacementLayer = mappingList.LayerA
}
}
- // Clone pattern and apply overrides
+ // Clone pattern and apply foundry and layer overrides
processedPattern := pattern.Clone()
if patternFoundry != "" || patternLayer != "" {
ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer)
@@ -108,9 +109,10 @@
continue // No matches, try next rule
}
- // Apply RestrictToObligatory to the replacement to get the annotations to add
- // Note: Only pass foundry override, not layer, since replacement terms have correct layers
- restrictedReplacement := ast.RestrictToObligatory(replacement, replacementFoundry, "")
+ // Apply RestrictToObligatory with layer precedence logic
+ restrictedReplacement := m.applyReplacementWithLayerPrecedence(
+ replacement, replacementFoundry, replacementLayer,
+ mappingID, ruleIndex, bool(opts.Direction))
if restrictedReplacement == nil {
continue // Nothing obligatory to add
}
@@ -239,3 +241,124 @@
return result, nil
}
+
+// applyReplacementWithLayerPrecedence applies RestrictToObligatory with proper layer precedence
+func (m *Mapper) applyReplacementWithLayerPrecedence(
+ replacement ast.Node, foundry, layerOverride string,
+ mappingID string, ruleIndex int, direction bool) ast.Node {
+
+ // First, apply RestrictToObligatory without layer override to preserve explicit layers
+ restricted := ast.RestrictToObligatory(replacement, foundry, "")
+ if restricted == nil {
+ return nil
+ }
+
+ // If no layer override is specified, we're done
+ if layerOverride == "" {
+ return restricted
+ }
+
+ // Apply layer override only to terms that didn't have explicit layers in the original rule
+ mappingList := m.mappingLists[mappingID]
+ if ruleIndex < len(mappingList.Mappings) {
+ originalRule := string(mappingList.Mappings[ruleIndex])
+ m.applySelectiveLayerOverrides(restricted, layerOverride, originalRule, direction)
+ }
+
+ return restricted
+}
+
+// applySelectiveLayerOverrides applies layer overrides only to terms without explicit layers
+func (m *Mapper) applySelectiveLayerOverrides(node ast.Node, layerOverride, originalRule string, direction bool) {
+ if node == nil {
+ return
+ }
+
+ // Parse the original rule without defaults to detect explicit layers
+ explicitTerms := m.getExplicitTerms(originalRule, direction)
+
+ // Apply overrides only to terms that weren't explicit in the original rule
+ termIndex := 0
+ m.applyLayerOverrideToImplicitTerms(node, layerOverride, explicitTerms, &termIndex)
+}
+
+// getExplicitTerms parses the original rule without defaults to identify terms with explicit layers
+func (m *Mapper) getExplicitTerms(originalRule string, direction bool) map[int]bool {
+ explicitTerms := make(map[int]bool)
+
+ // Parse without defaults to see what was explicitly specified
+ parser, err := parser.NewGrammarParser("", "")
+ if err != nil {
+ return explicitTerms
+ }
+
+ result, err := parser.ParseMapping(originalRule)
+ if err != nil {
+ return explicitTerms
+ }
+
+ // Get the replacement side based on direction
+ var replacement ast.Node
+ if direction { // AtoB
+ replacement = result.Lower.Wrap
+ } else { // BtoA
+ replacement = result.Upper.Wrap
+ }
+
+ // Extract terms and check which ones have explicit layers
+ termIndex := 0
+ m.markExplicitTerms(replacement, explicitTerms, &termIndex)
+ return explicitTerms
+}
+
+// markExplicitTerms recursively marks terms that have explicit layers
+func (m *Mapper) markExplicitTerms(node ast.Node, explicitTerms map[int]bool, termIndex *int) {
+ if node == nil {
+ return
+ }
+
+ switch n := node.(type) {
+ case *ast.Term:
+ // A term has an explicit layer if it was specified in the original rule
+ if n.Layer != "" {
+ explicitTerms[*termIndex] = true
+ }
+ *termIndex++
+
+ case *ast.TermGroup:
+ for _, operand := range n.Operands {
+ m.markExplicitTerms(operand, explicitTerms, termIndex)
+ }
+
+ case *ast.Token:
+ if n.Wrap != nil {
+ m.markExplicitTerms(n.Wrap, explicitTerms, termIndex)
+ }
+ }
+}
+
+// applyLayerOverrideToImplicitTerms applies layer override only to terms not marked as explicit
+func (m *Mapper) applyLayerOverrideToImplicitTerms(node ast.Node, layerOverride string, explicitTerms map[int]bool, termIndex *int) {
+ if node == nil {
+ return
+ }
+
+ switch n := node.(type) {
+ case *ast.Term:
+ // Apply override only if this term wasn't explicit in the original rule
+ if !explicitTerms[*termIndex] && n.Layer != "" {
+ n.Layer = layerOverride
+ }
+ *termIndex++
+
+ case *ast.TermGroup:
+ for _, operand := range n.Operands {
+ m.applyLayerOverrideToImplicitTerms(operand, layerOverride, explicitTerms, termIndex)
+ }
+
+ case *ast.Token:
+ if n.Wrap != nil {
+ m.applyLayerOverrideToImplicitTerms(n.Wrap, layerOverride, explicitTerms, termIndex)
+ }
+ }
+}
diff --git a/mapper/response_test.go b/mapper/response_test.go
index 76fd45e..566f235 100644
--- a/mapper/response_test.go
+++ b/mapper/response_test.go
@@ -9,484 +9,6 @@
"github.com/stretchr/testify/require"
)
-func XTestResponseMapping(t *testing.T) {
-
- responseSnippet := `{
- "@context": "http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld",
- "ID": null,
- "author": "Schmelzle, u.a.",
- "availability": "CC-BY-SA",
- "context": {
- "left": [
- "token",
- 0
- ],
- "right": [
- "token",
- 0
- ]
- },
- "corpusID": null,
- "corpusSigle": "WPD17",
- "docID": null,
- "docSigle": "WPD17/J80",
- "fields": [
- {
- "@type": "koral:field",
- "key": "ID"
- },
- {
- "@type": "koral:field",
- "key": "textSigle",
- "type": "type:string",
- "value": "WPD17/J80/33968"
- },
- {
- "@type": "koral:field",
- "key": "corpusID"
- },
- {
- "@type": "koral:field",
- "key": "author",
- "type": "type:text",
- "value": "Schmelzle, u.a."
- },
- {
- "@type": "koral:field",
- "key": "title",
- "type": "type:text",
- "value": "Johanne von Gemmingen"
- },
- {
- "@type": "koral:field",
- "key": "subTitle"
- },
- {
- "@type": "koral:field",
- "key": "textClass"
- },
- {
- "@type": "koral:field",
- "key": "pubPlace",
- "type": "type:string",
- "value": "URL:http://de.wikipedia.org"
- },
- {
- "@type": "koral:field",
- "key": "pubDate",
- "type": "type:date",
- "value": "2017-07-01"
- },
- {
- "@type": "koral:field",
- "key": "availability",
- "type": "type:string",
- "value": "CC-BY-SA"
- },
- {
- "@type": "koral:field",
- "key": "layerInfos",
- "type": "type:store",
- "value": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens"
- },
- {
- "@type": "koral:field",
- "key": "docSigle",
- "type": "type:string",
- "value": "WPD17/J80"
- },
- {
- "@type": "koral:field",
- "key": "corpusSigle",
- "type": "type:string",
- "value": "WPD17"
- }
- ],
- "hasSnippet": true,
- "hasTokens": false,
- "layerInfos": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens",
- "matchID": "p162-165(1)163-163x_yuvMM6VZLzLe_qZ0zb9yguvk37eDi-pSoL1nBdUkhNs",
- "meta": {
- "version": "Krill-0.64.1"
- },
- "pubDate": "2017-07-01",
- "pubPlace": "URL:http://de.wikipedia.org",
- "snippet": "<span class=\"context-left\">` +
- `</span>` +
- `<span class=\"match\">` +
- `<mark>` +
- `<span title=\"corenlp/p:ART\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:sg\">` +
- `<span title=\"marmot/p:ART\">` +
- `<span title=\"opennlp/p:ART\">` +
- `<span title=\"tt/l:die\">` +
- `<span title=\"tt/p:ART\">Der</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:ADJA\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:degree:pos\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:sg\">` +
- `<span title=\"marmot/p:ADJA\">` +
- `<span title=\"opennlp/p:ADJA\">` +
- `<span title=\"tt/l:alt\">` +
- `<span title=\"tt/p:ADJA\">alte</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:NN\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:sg\">` +
- `<span title=\"marmot/p:NN\">` +
- `<span title=\"opennlp/p:NN\">` +
- `<span title=\"tt/l:Baum\">` +
- `<span title=\"tt/p:NN\">Baum</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</mark> ` +
- `<span title=\"corenlp/p:KON\">` +
- `<span title=\"marmot/p:KON\">` +
- `<span title=\"opennlp/p:KON\">` +
- `<span title=\"tt/l:und\">` +
- `<span title=\"tt/p:KON\">und</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:ADJA\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:degree:pos\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:pl\">` +
- `<span title=\"marmot/p:ADJA\">` +
- `<span title=\"opennlp/p:ADJA\">` +
- `<span title=\"tt/l:andere\">` +
- `<span title=\"tt/p:PIAT\">` +
- `<span title=\"tt/p:PIS\">andere</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:NN\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:pl\">` +
- `<span title=\"marmot/p:NN\">` +
- `<span title=\"opennlp/p:NN\">` +
- `<span title=\"tt/l:Märchen\">` +
- `<span title=\"tt/p:NN\">Märchen</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>, ` +
- `<span title=\"corenlp/p:CARD\">` +
- `<span title=\"marmot/p:CARD\">` +
- `<span title=\"opennlp/p:CARD\">` +
- `<span title=\"tt/l:@card@\">` +
- `<span title=\"tt/p:CARD\">1946</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `</span>` +
- `<span class=\"context-right\"></span>",` +
- `"subTitle": null,
- "textClass": null,
- "textID": null,
- "textSigle": "WPD17/J80/33968",
- "title": "Johanne von Gemmingen"
-}`
-
- expectedOutput := `{
- "@context": "http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld",
- "ID": null,
- "author": "Schmelzle, u.a.",
- "availability": "CC-BY-SA",
- "context": {
- "left": [
- "token",
- 0
- ],
- "right": [
- "token",
- 0
- ]
- },
- "corpusID": null,
- "corpusSigle": "WPD17",
- "docID": null,
- "docSigle": "WPD17/J80",
- "fields": [
- {
- "@type": "koral:field",
- "key": "ID"
- },
- {
- "@type": "koral:field",
- "key": "textSigle",
- "type": "type:string",
- "value": "WPD17/J80/33968"
- },
- {
- "@type": "koral:field",
- "key": "corpusID"
- },
- {
- "@type": "koral:field",
- "key": "author",
- "type": "type:text",
- "value": "Schmelzle, u.a."
- },
- {
- "@type": "koral:field",
- "key": "title",
- "type": "type:text",
- "value": "Johanne von Gemmingen"
- },
- {
- "@type": "koral:field",
- "key": "subTitle"
- },
- {
- "@type": "koral:field",
- "key": "textClass"
- },
- {
- "@type": "koral:field",
- "key": "pubPlace",
- "type": "type:string",
- "value": "URL:http://de.wikipedia.org"
- },
- {
- "@type": "koral:field",
- "key": "pubDate",
- "type": "type:date",
- "value": "2017-07-01"
- },
- {
- "@type": "koral:field",
- "key": "availability",
- "type": "type:string",
- "value": "CC-BY-SA"
- },
- {
- "@type": "koral:field",
- "key": "layerInfos",
- "type": "type:store",
- "value": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens"
- },
- {
- "@type": "koral:field",
- "key": "docSigle",
- "type": "type:string",
- "value": "WPD17/J80"
- },
- {
- "@type": "koral:field",
- "key": "corpusSigle",
- "type": "type:string",
- "value": "WPD17"
- }
- ],
- "hasSnippet": true,
- "hasTokens": false,
- "layerInfos": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens",
- "matchID": "p162-165(1)163-163x_yuvMM6VZLzLe_qZ0zb9yguvk37eDi-pSoL1nBdUkhNs",
- "meta": {
- "version": "Krill-0.64.1"
- },
- "pubDate": "2017-07-01",
- "pubPlace": "URL:http://de.wikipedia.org",
- "snippet": "<span class=\"context-left\">` +
- `</span>` +
- `<span class=\"match\">` +
- `<mark>` +
- `<span title=\"corenlp/p:ART\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:sg\">` +
- `<span title=\"marmot/p:ART\">` +
- `<span title=\"opennlp/p:ART\">` +
- `<span title=\"tt/l:die\">` +
- `<span title=\"tt/p:ART\">` +
- `<span title=\"opennlp/p:M\" class=\"notinindex\">` +
- `<span title=\"opennlp/m:M\" class=\"notinindex\">Der</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:ADJA\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:degree:pos\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:sg\">` +
- `<span title=\"marmot/p:ADJA\">` +
- `<span title=\"opennlp/p:ADJA\">` +
- `<span title=\"tt/l:alt\">` +
- `<span title=\"tt/p:ADJA\">` +
- `<span title=\"opennlp/p:M\" class=\"notinindex\">` +
- `<span title=\"opennlp/m:M\" class=\"notinindex\">alte</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:NN\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:sg\">` +
- `<span title=\"marmot/p:NN\">` +
- `<span title=\"opennlp/p:NN\">` +
- `<span title=\"tt/l:Baum\">` +
- `<span title=\"tt/p:NN\">` +
- `<span title=\"opennlp/p:M\" class=\"notinindex\">` +
- `<span title=\"opennlp/m:M\" class=\"notinindex\">Baum</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</mark> ` +
- `<span title=\"corenlp/p:KON\">` +
- `<span title=\"marmot/p:KON\">` +
- `<span title=\"opennlp/p:KON\">` +
- `<span title=\"tt/l:und\">` +
- `<span title=\"tt/p:KON\">und</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:ADJA\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:degree:pos\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:pl\">` +
- `<span title=\"marmot/p:ADJA\">` +
- `<span title=\"opennlp/p:ADJA\">` +
- `<span title=\"tt/l:andere\">` +
- `<span title=\"tt/p:PIAT\">` +
- `<span title=\"tt/p:PIS\">` +
- `<span title=\"opennlp/p:M\" class=\"notinindex\">` +
- `<span title=\"opennlp/m:M\" class=\"notinindex\">andere</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `<span title=\"corenlp/p:NN\">` +
- `<span title=\"marmot/m:case:nom\">` +
- `<span title=\"marmot/m:gender:masc\">` +
- `<span title=\"marmot/m:number:pl\">` +
- `<span title=\"marmot/p:NN\">` +
- `<span title=\"opennlp/p:NN\">` +
- `<span title=\"tt/l:Märchen\">` +
- `<span title=\"tt/p:NN\">` +
- `<span title=\"opennlp/p:M\" class=\"notinindex\">` +
- `<span title=\"opennlp/p:M\" class=\"notinindex\">Märchen</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span>, ` +
- `<span title=\"corenlp/p:CARD\">` +
- `<span title=\"marmot/p:CARD\">` +
- `<span title=\"opennlp/p:CARD\">` +
- `<span title=\"tt/l:@card@\">` +
- `<span title=\"tt/p:CARD\">1946</span>` +
- `</span>` +
- `</span>` +
- `</span>` +
- `</span> ` +
- `</span>` +
- `<span class=\"context-right\"></span>",` +
- `"subTitle": null,
- "textClass": null,
- "textID": null,
- "textSigle": "WPD17/J80/33968",
- "title": "Johanne von Gemmingen"
-}`
-
- // Create test mapping list specifically for token to termGroup test
- mappingList := config.MappingList{
- ID: "test-mapper",
- FoundryA: "marmot",
- LayerA: "m",
- FoundryB: "opennlp", // Keep the same foundry for both sides
- LayerB: "p",
- Mappings: []config.MappingRule{
- "[gender=masc] <> [opennlp/p=M & opennlp/m=M]",
- },
- }
-
- // Create a new mapper
- m, err := NewMapper([]config.MappingList{mappingList})
- require.NoError(t, err)
-
- var inputData any
- err = json.Unmarshal([]byte(responseSnippet), &inputData)
- assert.Nil(t, err)
-
- result, err := m.ApplyResponseMappings("test-mapper", MappingOptions{Direction: AtoB}, inputData)
- assert.Nil(t, err)
-
- var expectedData any
- err = json.Unmarshal([]byte(expectedOutput), &expectedData)
-
- assert.Equal(t, expectedData, result)
- assert.Nil(t, err)
-}
-
// TestResponseMappingAnnotationCreation tests creating new annotations based on RestrictToObligatory
func TestResponseMappingAnnotationCreation(t *testing.T) {
// Simple snippet with a single annotated token
@@ -970,3 +492,86 @@
author := resultMap["author"].(string)
assert.Equal(t, "John Doe", author)
}
+
+// TestResponseMappingWithLayerOverride tests layer precedence rules
+func TestResponseMappingWithLayerOverride(t *testing.T) {
+ // Test 1: Explicit layer in mapping rule should take precedence over MappingOptions
+ t.Run("Explicit layer takes precedence", func(t *testing.T) {
+ responseSnippet := `{
+ "snippet": "<span title=\"marmot/p:DET\">Der</span>"
+ }`
+
+ // Mapping rule with explicit layer [p=DT] - this should NOT be overridden
+ mappingList := config.MappingList{
+ ID: "test-layer-precedence",
+ FoundryA: "marmot",
+ LayerA: "p",
+ FoundryB: "opennlp",
+ LayerB: "p", // default layer
+ Mappings: []config.MappingRule{
+ "[DET] <> [p=DT]", // Explicit layer "p" should not be overridden
+ },
+ }
+
+ m, err := NewMapper([]config.MappingList{mappingList})
+ require.NoError(t, err)
+
+ var inputData any
+ err = json.Unmarshal([]byte(responseSnippet), &inputData)
+ require.NoError(t, err)
+
+ // Apply with layer override - should NOT affect explicit layer in mapping rule
+ result, err := m.ApplyResponseMappings("test-layer-precedence", MappingOptions{
+ Direction: AtoB,
+ LayerB: "pos", // This should NOT override the explicit "p" layer in [p=DT]
+ }, inputData)
+ require.NoError(t, err)
+
+ resultMap := result.(map[string]any)
+ snippet := resultMap["snippet"].(string)
+
+ // Should use explicit layer "p" from mapping rule, NOT "pos" from override
+ assert.Contains(t, snippet, `title="opennlp/p:DT" class="notinindex"`)
+ assert.NotContains(t, snippet, `title="opennlp/pos:DT" class="notinindex"`)
+ })
+
+ // Test 2: Implicit layer in mapping rule should use MappingOptions layer override
+ t.Run("Implicit layer uses MappingOptions override", func(t *testing.T) {
+ responseSnippet := `{
+ "snippet": "<span title=\"marmot/p:DET\">Der</span>"
+ }`
+
+ // Mapping rule with implicit layer [DT] - this should use layer override
+ mappingList := config.MappingList{
+ ID: "test-layer-override",
+ FoundryA: "marmot",
+ LayerA: "p",
+ FoundryB: "opennlp",
+ LayerB: "p", // default layer
+ Mappings: []config.MappingRule{
+ "[DET] <> [DT]", // No explicit layer - should use override
+ },
+ }
+
+ m, err := NewMapper([]config.MappingList{mappingList})
+ require.NoError(t, err)
+
+ var inputData any
+ err = json.Unmarshal([]byte(responseSnippet), &inputData)
+ require.NoError(t, err)
+
+ // Apply with layer override - should affect implicit layer in mapping rule
+ result, err := m.ApplyResponseMappings("test-layer-override", MappingOptions{
+ Direction: AtoB,
+ LayerB: "pos", // This should override the default layer for [DT]
+ }, inputData)
+ require.NoError(t, err)
+
+ resultMap := result.(map[string]any)
+ snippet := resultMap["snippet"].(string)
+
+ // Should use layer "pos" from override, NOT default "p" layer
+ assert.Contains(t, snippet, `title="opennlp/pos:DT" class="notinindex"`)
+ assert.NotContains(t, snippet, `title="opennlp/p:DT" class="notinindex"`)
+ })
+}