Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ <h3 class="h5 fw-bold">{{ treeType() }}</h3>
</div>
<div class="p-4">
<div class="d-flex border border-secondary">
<la-tree-node [node]="displayTree()" />
<la-tree-node [node]="displayTree()" [treeType]="treeType()" />
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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' });
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
};
Expand All @@ -50,8 +50,8 @@ function buildBinaryNode(node: BinaryNode): TreeNodeDisplay {

return {
type: 'node',
content: content,
rule: rule,
content,
rule,
children: [left, right]
};
}
Expand All @@ -63,42 +63,42 @@ function buildUnaryNode(node: UnaryNode): TreeNodeDisplay {

return {
type: 'node',
content: content,
rule: rule,
content,
rule,
children: [child]
};
}

/**
* 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({
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,17 @@
@if (hasChildren) {
<div class="d-flex">
@for (childRow of node().children; track $index) {
<la-tree-node [node]="childRow" />
<la-tree-node class="w-100" [node]="childRow" [treeType]="treeType()" />
}
</div>
}
<div class="node-content d-flex flex-column align-items-center p-2 gap-1">
@if (node().rule) {
<span class="badge rounded-pill text-bg-secondary mt-2">
{{ node().rule }}
</span>
}
<div
class="node-content d-flex flex-column align-items-center px-2 py-1 gap-1"
>
@switch (node().type) {
@case ('leaf') {
<div class="d-flex flex-column align-items-center gap-2">
<div class="h5 mb-0">
<div class="d-flex flex-column align-items-center gap-1">
<div class="fw-bold mb-0">
<strong>{{ node().leaf?.lem }}</strong>
</div>
<div class="text-secondary">
Expand All @@ -29,21 +26,37 @@
<br />
<small>{{ node().leaf?.ner }}</small>
</div>
<div [innerHTML]="node().content | subscriptAngleBrackets"></div>
@if (node().content; as nodeContent) {
<div [innerHTML]="nodeContent | subscript" class="text-nowrap"></div>
}
</div>
} @case ('var') {
<div class="d-flex flex-column gap-1">
<strong class="h5 fw-bold mb-0">
{{ node().content }}
@if (node().rule) {
<span class="badge rounded-pill text-bg-secondary mt-2">
{{ node().rule }}
</span>
}
</strong>
<br />
<span>{{ node().var?.typeInfo }}</span>
</div>
} @case ('node') {
<p
class="mb-0"
[innerHTML]="node().content | subscriptAngleBrackets"
></p>
<p class="my-0">
@if (node().rule; as nodeRule) {
<span class="badge rounded-pill text-bg-secondary me-2">
{{ nodeRule }}
</span>
}
@if (node().content; as nodeContent) {
<span
class="mb-0 text-nowrap"
[innerHTML]="nodeContent | selectiveUpperCase: treeType() | subscript"
></span>
}
</p>
}
}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<TreeNodeDisplay>();
public readonly treeType = input.required<TreeType>();
}
56 changes: 56 additions & 0 deletions frontend/src/app/pipes/selective-upper-case.pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
32 changes: 32 additions & 0 deletions frontend/src/app/pipes/selective-upper-case.pipe.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading