Skip to content

Commit

Permalink
FormattingNode#unformat method & some tests refactoring (#11)
Browse files Browse the repository at this point in the history
FormattingNode#unformat mehtod & some tests refactoring
  • Loading branch information
gohabereg authored Aug 24, 2023
1 parent 4cb9623 commit 1d42db0
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 75 deletions.
114 changes: 92 additions & 22 deletions src/entities/FormattingNode/FormattingNode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
import { beforeEach, describe, expect, it } from '@jest/globals';
import { ParentNode } from '../interfaces';
import type { TextNode } from '../TextNode';
import { createInlineToolData, createInlineToolName, FormattingNode } from './index';
import { TextNode } from '../TextNode';

const parentMock = {
insertAfter: jest.fn(),
removeChild: jest.fn(),
append: jest.fn(),
children: [],
} as unknown as ParentNode;

const createChildMock = (value: string): TextNode => ({
getText: jest.fn(() => value),
appendTo: jest.fn(),
insertText: jest.fn(),
removeText: jest.fn(),
split: jest.fn(() => null),
format: jest.fn(() => [ new FormattingNode({ tool: createInlineToolName('tool') }) ]),
length: value.length,
} as unknown as TextNode);
import { createTextNodeMock } from '../../mocks/TextNode.mock';
import { createParentNodeMock } from '../../mocks/ParentNode.mock';
import type { ParentNode } from '../interfaces';

describe('FormattingNode', () => {
const childMock = createChildMock('Some text here. ');
const anotherChildMock = createChildMock('Another text here.');
let parentMock: ParentNode;
let childMock: TextNode;
let anotherChildMock: TextNode;

const tool = createInlineToolName('bold');
const anotherTool = createInlineToolName('italic');
const data = createInlineToolData({});
let node: FormattingNode;

beforeEach(() => {
parentMock = createParentNodeMock() as FormattingNode;
childMock = createTextNodeMock('Some text here. ');
anotherChildMock = createTextNodeMock('Another text here.');

node = new FormattingNode({
tool,
data,
parent: parentMock as FormattingNode,
parent: parentMock,
children: [childMock, anotherChildMock],
});

Expand Down Expand Up @@ -278,4 +268,84 @@ describe('FormattingNode', () => {
expect(result).toEqual(childMock.format(anotherTool, start, end));
});
});

describe('.unformat()', () => {
const start = 3;
const end = 5;
let childFormattingNode: FormattingNode;
let anotherChildFormattingNode: FormattingNode;

beforeEach(() => {
childFormattingNode = new FormattingNode({
tool: anotherTool,
data,
children: [ createTextNodeMock('Some text here. ') ],
});

anotherChildFormattingNode = new FormattingNode({
tool: anotherTool,
data,
children: [ createTextNodeMock('Another text here. ') ] }
);


node = new FormattingNode({
tool,
data,
parent: parentMock as FormattingNode,
children: [childFormattingNode, anotherChildFormattingNode],
});

jest.spyOn(childFormattingNode, 'unformat');
jest.spyOn(anotherChildFormattingNode, 'unformat');
});

it('should remove formatting from the relevant child', () => {
node.unformat(anotherTool, start, end);

expect(childFormattingNode.unformat).toBeCalledWith(anotherTool, start, end);
});

it('should adjust index by child offset', () => {
const offset = childFormattingNode.length;

node.unformat(anotherTool, offset + start, offset + end);

expect(anotherChildFormattingNode.unformat).toBeCalledWith(anotherTool, start, end);
});

it('should call unformat for all relevant children', () => {
const offset = childMock.length;

node.unformat(anotherTool, start, offset + end);

expect(childFormattingNode.unformat).toBeCalledWith(anotherTool, start, offset);
expect(anotherChildFormattingNode.unformat).toBeCalledWith(anotherTool, 0, end);
});

it('should do nothing if different tool is being unformatted', () => {
node.unformat(tool, start, end);

expect(childFormattingNode.unformat).not.toBeCalled();
expect(anotherChildFormattingNode.unformat).not.toBeCalled();
});

it('should return array of new nodes with unformatted part', () => {
const result = node.unformat(anotherTool, start, end);

expect(result).toEqual([
expect.any(FormattingNode),
/**
* On this place is unformatted TextNode mock
*/
expect.any(Object),
expect.any(FormattingNode)]);
});

it('should do nothing for TextNode children', () => {
const result = childFormattingNode.unformat(tool, start, end);

expect(result).toEqual([]);
});
});
});
54 changes: 53 additions & 1 deletion src/entities/FormattingNode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export class FormattingNode implements InlineNode {
public format(tool: InlineToolName, start: number, end: number, data?: InlineToolData): InlineNode[] {
/**
* In case current tool is the same as new one, do nothing
*
* @todo Compare data as well
*/
if (tool === this.#tool) {
return [];
Expand All @@ -211,6 +213,51 @@ export class FormattingNode implements InlineNode {
);
}

/**
* Removes formatting from the text for a specified inline tool in the specified range
*
* @param tool - name of inline tool to remove
* @param start - char start index of the range
* @param end - char end index of the range
* @todo Possibly pass data or some InlineTool identifier to relevant only required fragments
*/
public unformat(tool: InlineToolName, start: number, end: number): InlineNode[] {
if (this.#tool === tool) {
const middleNode = this.split(start);
const endNode = middleNode?.split(end);

const result: InlineNode[] = [ this ];

if (middleNode) {
result.push(...middleNode.children);
}

if (endNode) {
result.push(endNode);
}

return result;
}

return this.#reduceChildrenInRange<InlineNode[]>(
start,
end,
(acc, child, childStart, childEnd) => {
/**
* TextNodes don't have unformat method, so skip them
*/
if (!(child instanceof FormattingNode)) {
return acc;
}

acc.push(...child.unformat(tool, childStart, childEnd));

return acc;
},
[]
);
}

/**
* Iterates through children in range and calls callback for each
*
Expand All @@ -228,7 +275,12 @@ export class FormattingNode implements InlineNode {
): Acc {
let result = initialValue;

for (const child of this.children) {
/**
* Make a copy of the children array in case callback would modify it
*/
const children = Array.from(this.children);

for (const child of children) {
if (start < child.length && end > 0 && start < end) {
result = callback(result, child, Math.max(start, 0), Math.min(child.length, end));
}
Expand Down
11 changes: 4 additions & 7 deletions src/entities/interfaces/ChildNode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { ChildNode } from './ChildNode';
import type { ParentNode } from './ParentNode';

const parentMock = {
append: jest.fn(),
removeChild: jest.fn(),
insertAfter: jest.fn(),
children: [],
} as unknown as ParentNode;
import { createParentNodeMock } from '../../mocks/ParentNode.mock';

interface Dummy extends ChildNode {
}
Expand All @@ -27,8 +21,11 @@ class Dummy {

describe('ChildNode decorator', () => {
let dummy: Dummy;
let parentMock: ParentNode;

beforeEach(() => {
parentMock = createParentNodeMock();

jest.resetAllMocks();
});

Expand Down
2 changes: 1 addition & 1 deletion src/entities/interfaces/ChildNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function ChildNode<C extends { new(...args: any[]): InlineNode }>(constru
// Stryker disable next-line BlockStatement -- Styker's bug, see https://github.com/stryker-mutator/stryker-js/issues/2474
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- any type here is a TS requirement for mixin classes
constructor(...args: any[]) {
const { parent, ...rest } = args[0] ?? {};
const { parent, ...rest } = args[0] as ChildNodeConstructorOptions ?? {};

super(rest);

Expand Down
11 changes: 11 additions & 0 deletions src/entities/interfaces/InlineNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,17 @@ export interface InlineNode {
*/
format(name: InlineToolName, start?: number, end?: number, data?: InlineToolData): InlineNode[];

/**
* Removes inline formatting from the passed range
*
* Optional as some nodes don't contain any formatting (e.g. TextNode)
*
* @param name - name of Inline Tool to remove
* @param start - start char index of the range
* @param end - end char index of the range
*/
unformat?(name: InlineToolName, start?: number, end?: number): InlineNode[];

/**
* Inserts text at passed char index
*
Expand Down
Loading

0 comments on commit 1d42db0

Please sign in to comment.