@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];
};