diff --git a/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts b/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts index e96ec5af..1b7da54a 100644 --- a/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts +++ b/frontend/src/app/annotate/annotation-parse-results/annotation-parse-results.component.ts @@ -7,8 +7,10 @@ import { map } from "rxjs"; import { CommonModule } from "@angular/common"; import { CCGNode, CCGParse } from "@/types"; +export type TreeType = "CCG Tree" | "CCG Term" | "Corrected CCG Term" | "Lambda Logical Form"; + export interface TreeWithType { - type: string; + type: TreeType; tree: CCGNode; } @@ -19,7 +21,6 @@ interface UnfoldedParseResult { function unfoldParseResult(parse: CCGParse): UnfoldedParseResult { const { ccg_tree, ccg_term, corr_term, llf } = parse.ccg_trees; - // TODO: Reintroduce the other trees once they are serialized properly. return { ...parse, ccgTrees: [ diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.html b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.html index cdac489a..5a053b83 100644 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.html +++ b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.html @@ -4,7 +4,7 @@

{{ treeType() }}

- +
diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.spec.ts b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.spec.ts index d91fe68a..c4565038 100644 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.spec.ts +++ b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.spec.ts @@ -1,6 +1,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ParseTreeTableComponent } from './parse-tree-table.component'; +import { ParseTreeTableComponent, extractRule } from './parse-tree-table.component'; import { TreeWithType } from '../annotation-parse-results.component'; const mockTree: TreeWithType = { @@ -35,3 +35,46 @@ describe('ParseTreeTableComponent', () => { expect(component).toBeTruthy(); }); }); + +describe('extractRule', () => { + describe('standard format', () => { + it('should extract rule and content from standard format', () => { + const result = extractRule('fa[s:ng-np]'); + expect(result).toEqual({ rule: 'fa', content: 's:ng-np' }); + }); + + it('should extract rule with complex content', () => { + const result = extractRule('fa[(s:dcl\\np)/np]'); + expect(result).toEqual({ rule: 'fa', content: '(s:dcl\\np)/np' }); + }); + }); + + describe('trivial @ rule', () => { + it('should return empty rule for @ symbol', () => { + const result = extractRule('@[np:nb]'); + expect(result).toEqual({ rule: '', content: 'np:nb' }); + }); + }); + + describe('handle extra brackets', () => { + it('should strip extra opening brackets from content', () => { + const result = extractRule('fa[s:[ng-np]'); + expect(result).toEqual({ rule: 'fa', content: 's:ng-np' }); + }); + + it('should strip extra closing brackets from content', () => { + const result = extractRule('fa[s:ng]-np]'); + expect(result).toEqual({ rule: 'fa', content: 's:ng-np' }); + }); + + it('should strip multiple extra brackets from content', () => { + const result = extractRule('fa[s:[ng]-[np]]'); + expect(result).toEqual({ rule: 'fa', content: 's:ng-np' }); + }); + + it('should strip all internal brackets', () => { + const result = extractRule('ba[[[s:dcl]]]'); + expect(result).toEqual({ rule: 'ba', content: 's:dcl' }); + }); + }); +}); diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.ts b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.ts index 2b647d82..9f33512e 100644 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.ts +++ b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/parse-tree-table.component.ts @@ -33,10 +33,10 @@ function nodeIsUnary(node: CCGNode): node is UnaryNode { } function buildLeafNode(node: LeafNode): TreeNodeDisplay { - const [_rule, tok, lem, pos, ner, cat] = node.node; + // "category" (chunker output) is deliberately unused. + const [tok, lem, pos, _cat, ner] = node.node; return { type: 'leaf', - content: cat, children: [], leaf: { tok, lem, pos, ner } }; @@ -50,8 +50,8 @@ function buildBinaryNode(node: BinaryNode): TreeNodeDisplay { return { type: 'node', - content: content, - rule: rule, + content, + rule, children: [left, right] }; } @@ -63,8 +63,8 @@ function buildUnaryNode(node: UnaryNode): TreeNodeDisplay { return { type: 'node', - content: content, - rule: rule, + content, + rule, children: [child] }; } @@ -72,33 +72,33 @@ function buildUnaryNode(node: UnaryNode): TreeNodeDisplay { /** * Parses a node string to extract the rule and the content. * - * A node string is usually of the form "A(B)", where a is the rule applied + * A node string is usually of the form "A[B]", where a is the rule applied * and B is the resulting category. The rule is anything everything before - * the first parenthesis. Everything within it is the content. For example, - * in "fa(s:ng-np)", "fa" is the rule and "s:ng-np" is the content. + * the first bracket. Everything within it is the content. For example, + * in "fa[s:ng-np]", "fa" is the rule and "s:ng-np" is the content. * - * Due to a bug in the CCG parser, sometimes the node string can have - * multiple layers of parentheses, e.g. fa(((s:ng-np)-(s:ng-np))). - * function only strips off the first. + * If there are more brackets, we ignore them. + * + * The rule symbolised by '@' is trivial and all too common, so it is ignored. * */ -function extractRule(nodeString: string): { rule: string, content: string; } { - const firstParen = nodeString.indexOf('('); - const lastParen = nodeString.lastIndexOf(')'); +export function extractRule(nodeString: string): { rule: string, content: string; } { + const firstBracket = nodeString.indexOf('['); + const lastBracket = nodeString.lastIndexOf(']'); // Return a fallback value if the string is not what we expect. - if (firstParen === -1 || lastParen === -1 || lastParen < firstParen) { + if (firstBracket === -1 || lastBracket === -1 || lastBracket < firstBracket) { return { rule: "", content: nodeString }; } - const rule = nodeString.slice(0, firstParen); - // Strip off any remaining parentheses due to the CCG parser bug. - const content = nodeString.slice(firstParen + 1, lastParen).replaceAll('(', '').replaceAll(')', ''); + const rule = nodeString.slice(0, firstBracket); + // Strip off any remaining brackets. + const content = nodeString.slice(firstBracket + 1, lastBracket).replaceAll('[', '').replaceAll(']', ''); - return { rule, content }; + return { rule: rule === '@' ? "" : rule, content }; } @Component({ diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/subscript-angle-brackets.pipe.ts b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/subscript-angle-brackets.pipe.ts deleted file mode 100644 index cf150cc5..00000000 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/subscript-angle-brackets.pipe.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core'; - -@Pipe({ - name: 'subscriptAngleBrackets', - standalone: true -}) -export class SubscriptAngleBracketsPipe implements PipeTransform { - /** - * Transforms text by converting content within angle brackets <...> to subscript. - * Example: "NP" becomes "NPnb" - */ - transform(value: string): string { - if (!value) { - return value; - } - return value.replace(/<([^>]+)>/g, '$1'); - } -} diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.html b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.html index dacbd9ec..c691952f 100644 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.html +++ b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.html @@ -6,20 +6,17 @@ @if (hasChildren) {
@for (childRow of node().children; track $index) { - + }
} -
- @if (node().rule) { - - {{ node().rule }} - - } +
@switch (node().type) { @case ('leaf') { -
-
+
+
{{ node().leaf?.lem }}
@@ -29,21 +26,37 @@
{{ node().leaf?.ner }}
-
+ @if (node().content; as nodeContent) { +
+ }
} @case ('var') {
{{ node().content }} + @if (node().rule) { + + {{ node().rule }} + + }
{{ node().var?.typeInfo }}
} @case ('node') { -

+

+ @if (node().rule; as nodeRule) { + + {{ nodeRule }} + + } + @if (node().content; as nodeContent) { + + } +

} }
diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.scss b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.scss index a6f0d93c..86643b45 100644 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.scss +++ b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.scss @@ -1,6 +1,6 @@ .tree-node { transition: background-color 0.2s ease; - min-height: 10em; + min-height: 7rem; text-align: center; // Only highlight the current hovered node diff --git a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.ts b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.ts index 088c168b..a7bb5829 100644 --- a/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.ts +++ b/frontend/src/app/annotate/annotation-parse-results/parse-tree-table/tree-node.component.ts @@ -1,9 +1,11 @@ import { Component, input } from '@angular/core'; -import { SubscriptAngleBracketsPipe } from './subscript-angle-brackets.pipe'; +import { SubscriptPipe } from '@/pipes/subscript-pipe'; +import { SelectiveUpperCasePipe } from '@/pipes/selective-upper-case.pipe'; +import { TreeType } from '../annotation-parse-results.component'; export interface TreeNodeDisplay { type: 'node' | 'leaf' | 'var'; - content: string; + content?: string; rule?: string; children: TreeNodeDisplay[]; // For leaf nodes @@ -22,10 +24,11 @@ export interface TreeNodeDisplay { @Component({ selector: 'la-tree-node', standalone: true, - imports: [SubscriptAngleBracketsPipe], + imports: [SubscriptPipe, SelectiveUpperCasePipe], templateUrl: './tree-node.component.html', styleUrl: './tree-node.component.scss' }) export class TreeNodeComponent { public readonly node = input.required(); + public readonly treeType = input.required(); } diff --git a/frontend/src/app/pipes/selective-upper-case.pipe.spec.ts b/frontend/src/app/pipes/selective-upper-case.pipe.spec.ts new file mode 100644 index 00000000..a7af0be6 --- /dev/null +++ b/frontend/src/app/pipes/selective-upper-case.pipe.spec.ts @@ -0,0 +1,56 @@ +import { SelectiveUpperCasePipe } from './selective-upper-case.pipe'; + +describe('SelectiveUpperCasePipe', () => { + let pipe: SelectiveUpperCasePipe; + + beforeEach(() => { + pipe = new SelectiveUpperCasePipe(); + }); + + it('should create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + describe('falsy values', () => { + it('should return null for null input', () => { + expect(pipe.transform(null as any, "CCG Tree")).toBeNull(); + }); + + it('should return undefined for undefined input', () => { + expect(pipe.transform(undefined as any, "CCG Tree")).toBeUndefined(); + }); + + it('should return empty string for empty string input', () => { + expect(pipe.transform('', "CCG Tree")).toBe(''); + }); + }); + + describe('uppercase conversion', () => { + it('should convert lowercase text to uppercase', () => { + const result = pipe.transform('hello', "CCG Tree"); + expect(result).toBe('HELLO'); + }); + + it('should handle text with special characters and numbers', () => { + const result = pipe.transform('hello@world123', "CCG Tree"); + expect(result).toBe('HELLO@WORLD123'); + }); + }); + + describe('whitelist and non-CCG Tree handling', () => { + it('should keep "period" in lowercase', () => { + const result = pipe.transform('period', "CCG Tree"); + expect(result).toBe('period'); + }); + + it('should convert "PERIOD" to lowercase', () => { + const result = pipe.transform('PERIOD', "CCG Tree"); + expect(result).toBe('period'); + }); + + it('should not convert text for non-CCG Tree types', () => { + const result = pipe.transform('hello', "CCG Term"); + expect(result).toBe('hello'); + }); + }); +}); diff --git a/frontend/src/app/pipes/selective-upper-case.pipe.ts b/frontend/src/app/pipes/selective-upper-case.pipe.ts new file mode 100644 index 00000000..16ff7715 --- /dev/null +++ b/frontend/src/app/pipes/selective-upper-case.pipe.ts @@ -0,0 +1,32 @@ +import { TreeType } from "@/annotate/annotation-parse-results/annotation-parse-results.component"; +import { Pipe } from "@angular/core"; + +const WHITELIST = ["period", "conj"]; + +@Pipe({ + name: "selectiveUpperCase", + standalone: true +}) +export class SelectiveUpperCasePipe { + /** + * Transforms text by converting all content to uppercase, except for items + * in the whitelist, and only if for the "CCG Tree" type. + */ + transform(value: string, treeType: TreeType): string { + if (!value) { + return value; + } + + // Only apply selective uppercase transformation for "CCG Tree" type. + if (treeType !== "CCG Tree") { + return value; + } + + // Whitelisted items should be returned in lowercase. + if (WHITELIST.includes(value.toLocaleLowerCase())) { + return value.toLocaleLowerCase(); + } + + return value.toLocaleUpperCase(); + } +} diff --git a/frontend/src/app/pipes/subscript-pipe.spec.ts b/frontend/src/app/pipes/subscript-pipe.spec.ts new file mode 100644 index 00000000..0d51cfab --- /dev/null +++ b/frontend/src/app/pipes/subscript-pipe.spec.ts @@ -0,0 +1,100 @@ +import { SubscriptPipe } from './subscript-pipe'; + +describe('SubscriptPipe', () => { + let pipe: SubscriptPipe; + + beforeEach(() => { + pipe = new SubscriptPipe(); + }); + + it('should create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + describe('nullish values', () => { + it('should return null for null input', () => { + expect(pipe.transform(null as any)).toBeNull(); + }); + + it('should return undefined for undefined input', () => { + expect(pipe.transform(undefined as any)).toBeUndefined(); + }); + + it('should return empty string for empty string input', () => { + expect(pipe.transform('')).toBe(''); + }); + }); + + describe('single matches', () => { + it('should convert text after colon and before backslash to subscript', () => { + const result = pipe.transform('np:dcl\\nb'); + expect(result).toBe('npdcl\\nb'); + }); + + it('should convert text after colon and before forward slash to subscript', () => { + const result = pipe.transform('np:dcl/nb'); + expect(result).toBe('npdcl/nb'); + }); + + it('should convert text after colon at end of string to subscript', () => { + const result = pipe.transform('s:ng'); + expect(result).toBe('sng'); + }); + + it('should convert text after colon and before opening parenthesis to subscript', () => { + const result = pipe.transform('np:nb(test)'); + expect(result).toBe('npnb(test)'); + }); + + it('should convert text after colon and before closing parenthesis to subscript', () => { + const result = pipe.transform('test:value)more'); + expect(result).toBe('testvalue)more'); + }); + + it('should convert text after colon and before hyphen to subscript', () => { + const result = pipe.transform('np:nb-s:dcl'); + expect(result).toBe('npnb-sdcl'); + }); + }); + + describe('multiple matches', () => { + it('should handle multiple colon patterns in one string', () => { + const result = pipe.transform('np:nb-s:dcl'); + expect(result).toBe('npnb-sdcl'); + }); + + it('should handle multiple colon patterns with different terminators', () => { + const result = pipe.transform('(np:nb-s:dcl)-s:dcl'); + expect(result).toBe('(npnb-sdcl)-sdcl'); + }); + }); + + describe('case conversion', () => { + it('should lowercase text after the colon', () => { + const result = pipe.transform('NP:DCL'); + expect(result).toBe('NPdcl'); + }); + + it('should preserve case of text before colon', () => { + const result = pipe.transform('NP:dcl'); + expect(result).toBe('NPdcl'); + }); + + it('should preserve case of text after terminator', () => { + const result = pipe.transform('np:dcl\\NB'); + expect(result).toBe('npdcl\\NB'); + }); + }); + + describe('edge cases', () => { + it('should handle string with no colon', () => { + const result = pipe.transform('nocontent'); + expect(result).toBe('nocontent'); + }); + + it('should handle special characters in subscript content', () => { + const result = pipe.transform('test:a@b#c'); + expect(result).toBe('testa@b#c'); + }); + }); +}); diff --git a/frontend/src/app/pipes/subscript-pipe.ts b/frontend/src/app/pipes/subscript-pipe.ts new file mode 100644 index 00000000..8b9e791e --- /dev/null +++ b/frontend/src/app/pipes/subscript-pipe.ts @@ -0,0 +1,21 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'subscript', + standalone: true +}) +export class SubscriptPipe implements PipeTransform { + /** + * Transforms text by converting all content in between a colon and a + * slash, a parenthesis, a hyphen or the end of the string to subscript. + * Example: "np:dcl\nb" becomes "npdclnb" + */ + transform(value: string): string { + if (!value) { + return value; + } + + return value.replace(/:(.*?)(\\|\/|$|\)|\(|-)/g, (_, p1, p2) => `${p1.toLocaleLowerCase()}${p2}`); + + } +} diff --git a/frontend/src/app/types.ts b/frontend/src/app/types.ts index f097e28d..a58b7272 100644 --- a/frontend/src/app/types.ts +++ b/frontend/src/app/types.ts @@ -137,7 +137,7 @@ export interface Dimensions { // export type LeafNode = { - // Fixed order: rule, token, lemma, POS tag, NER tag, category. + // Fixed order: rule, lemma, token, POS tag, NER tag, category. node: [string, string, string, string, string, string]; };