Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RootInlineNode #12

Merged
merged 9 commits into from
Aug 29, 2023
Merged
12 changes: 6 additions & 6 deletions src/entities/BlockNode/BlockNode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { BlockNode } from './index';
import { createBlockNodeName, createDataKey } from './types';

import { BlockTune, createBlockTuneName } from '../BlockTune';
import { TextNode } from '../TextNode';
import { TextInlineNode } from '../inline-fragments/TextInlineNode';
import { ValueNode } from '../ValueNode';

import type { EditorDocument } from '../EditorDocument';
import type { BlockTuneConstructorParameters } from '../BlockTune/types';
import type { TextNodeConstructorParameters } from '../TextNode';
import type { TextInlineNodeConstructorParameters } from '../inline-fragments/TextInlineNode';
import type { ValueNodeConstructorParameters } from '../ValueNode';

describe('BlockNode', () => {
Expand All @@ -18,8 +18,8 @@ describe('BlockNode', () => {
serialized: jest.fn(),
}));

jest.mock('../TextNode', () => ({
TextNode: jest.fn().mockImplementation(() => ({}) as TextNode),
jest.mock('../inline-fragments/TextInlineNode', () => ({
TextInlineNode: jest.fn().mockImplementation(() => ({}) as TextInlineNode),
serialized: jest.fn(),
}));

Expand Down Expand Up @@ -109,11 +109,11 @@ describe('BlockNode', () => {
});
});

it('should call .serialized getter of all child TextNodes associated with the BlockNode', () => {
it('should call .serialized getter of all child TextInlineNodes associated with the BlockNode', () => {
const countOfTextNodes = 3;

const textNodes = [ ...Array(countOfTextNodes).keys() ]
.map(() => new TextNode({} as TextNodeConstructorParameters));
.map(() => new TextInlineNode({} as TextInlineNodeConstructorParameters));

const spyArray = textNodes
.map((textNode) => {
Expand Down
6 changes: 3 additions & 3 deletions src/entities/BlockNode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {

/**
* BlockNode class represents a node in a tree-like structure used to store and manipulate Blocks in an editor document.
* A BlockNode can contain one or more child nodes of type TextNode, ValueNode or FormattingNode.
* A BlockNode can contain one or more child nodes of type TextInlineNode, ValueNode or FormattingInlineNode.
* It can also be associated with one or more BlockTunes, which can modify the behavior of the BlockNode.
*/
export class BlockNode {
Expand All @@ -38,7 +38,7 @@ export class BlockNode {
/**
* Constructor for BlockNode class.
*
* @param args - TextNode constructor arguments.
* @param args - BlockNode constructor arguments.
* @param args.name - The name of the BlockNode.
* @param args.data - The content of the BlockNode.
* @param args.parent - The parent EditorDocument of the BlockNode.
Expand All @@ -61,7 +61,7 @@ export class BlockNode {
(acc, [dataKey, value]) => {
/**
* If the value is an array, we need to serialize each node in the array
* Value is an array if the BlockNode contains TextNodes and FormattingNodes
* Value is an array if the BlockNode contains TextInlineNodes and FormattingInlineNodes
* After serializing there will be InlineNodeSerialized object
*/
if (value instanceof Array) {
Expand Down
5 changes: 2 additions & 3 deletions src/entities/BlockNode/types/BlockNodeData.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { DataKey } from './DataKey';
import { TextNode } from '../../TextNode';
import { ValueNode } from '../../ValueNode';
import { FormattingNode } from '../../FormattingNode';
import { TextInlineNode, FormattingInlineNode } from '../../inline-fragments';

/**
* Represents a record object containing the data of a block node.
* Each root node is associated with a specific data key.
*/
export type BlockNodeData = Record<DataKey, ValueNode | (FormattingNode | TextNode)[]>;
export type BlockNodeData = Record<DataKey, ValueNode | (FormattingInlineNode | TextInlineNode)[]>;
8 changes: 0 additions & 8 deletions src/entities/TextNode/types/TextNodeConstructorParameters.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/entities/TextNode/types/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/entities/ValueNode/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ValueNodeConstructorParameters } from './types';

/**
* ValueNode class represents a node in a tree-like structure, used to store and manipulate data associated with a BlockNode.
* Unlike TextNode, changing the data of a ValueNode will replace the entire data in this node.
* Unlike TextInlineNode, changing the data of a ValueNode will replace the entire data in this node.
* This can be useful for storing data that needs to be updated in its entirety, such as a link or other metadata associated with a BlockNode.
*/
export class ValueNode<ValueType = unknown> {
Expand Down
247 changes: 247 additions & 0 deletions src/entities/inline-fragments/FormattingInlineNode/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import {
FormattingInlineNodeConstructorParameters,
InlineToolName,
InlineToolData
} from './types';
import type { InlineFragment, InlineNode } from '../InlineNode';
import { ParentNode } from '../mixins/ParentNode';
import { ChildNode } from '../mixins/ChildNode';
import { ParentInlineNode } from '../ParentInlineNode';

export * from './types';

/**
* We need to extend FormattingInlineNode interface with ChildNode and ParentNode ones to use the methods from mixins
*/
export interface FormattingInlineNode extends ChildNode {}

/**
* FormattingInlineNode class represents a node in a tree-like structure, used to store and manipulate formatted text content
*/
@ParentNode
@ChildNode
export class FormattingInlineNode extends ParentInlineNode implements InlineNode {
/**
* Property representing the name of the formatting tool applied to the content
*/
public readonly tool: InlineToolName;

/**
* Any additional data associated with the formatting tool
*/
public readonly data?: InlineToolData;

/**
* Constructor for FormattingInlineNode class.
*
* @param args - FormattingInlineNode constructor arguments.
* @param args.tool - The name of the formatting tool applied to the content.
* @param args.data - Any additional data associated with the formatting.
*/
// Stryker disable next-line BlockStatement -- Styker's bug, see https://github.com/stryker-mutator/stryker-js/issues/2474
constructor({ tool, data }: FormattingInlineNodeConstructorParameters) {
super();

this.tool = tool;
this.data = data;
}

/**
* Removes text from the specified range. If there is no text left in a node, removes a node from a parent.
*
* @param [start] - start char index of the range, by default 0
* @param [end] - end char index of the range, by default length of the text value
* @returns {string} removed text
*/
public removeText(start = 0, end = this.length): string {
const result = super.removeText(start, end);

if (this.length === 0) {
this.remove();
}

return result;
}

/**
* Returns inline fragments for node from the specified character range
*
* If start and/or end is specified, method will return partial fragments for the specified range
*
* @param [start] - start char index of the range, by default 0
* @param [end] - end char index of the range, by default length of the text value
*/
public getFragments(start = 0, end = this.length): InlineFragment[] {
const fragments = super.getFragments(start, end);

const currentFragment: InlineFragment = {
tool: this.tool,
range: [start, end],
};

if (this.data) {
currentFragment.data = this.data;
}

/**
* Current node is not processed in super.getFragments, so we need to add it manually at the beginning
*/
fragments.unshift(currentFragment);

return fragments;
}

/**
* Splits current node by the specified index
*
* @param index - char index where to split the node
* @returns {FormattingInlineNode | null} new node
*/
public split(index: number): FormattingInlineNode | null {
if (index === 0 || index === this.length) {
return null;
}

const newNode = new FormattingInlineNode({
tool: this.tool,
data: this.data,
});

const [child, offset] = this.findChildByIndex(index);

if (!child) {
return null;
}

// Have to save length as it is changed after split
const childLength = child.length;

const splitNode = child.split(index - offset);
let midNodeIndex = this.children.indexOf(child);

/**
* If node is split or if node is not split but index equals to child length, we should split children from the next node
*/
if (splitNode || (index - offset === childLength)) {
midNodeIndex += 1;
}

newNode.append(...this.children.slice(midNodeIndex));

this.parent?.insertAfter(this, newNode);

return newNode;
}

/**
* Applies formatting to the text with specified inline tool in the specified range
*
* @param tool - name of inline tool to apply
* @param start - char start index of the range
* @param end - char end index of the range
* @param [data] - inline tool data if applicable
*/
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 [];
}


return super.format(tool, start, end, data);
}

/**
* 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: ChildNode[] = [];

/**
* If start > 0, then there is a middle node, so we'll need to append its children to the parent
*/
if (middleNode) {
result.push(this, ...middleNode.children);
/**
* Else we'll need to append current nodes children to the parent
*/
} else {
result.push(...this.children);
}

/**
* If end < this.length, we just append it to the parent
*/
if (endNode) {
result.push(endNode);
}

this.parent?.insertAfter(this, ...result);

if (middleNode) {
middleNode.remove();
} else {
this.remove();
}

return result;
}

return super.unformat(tool, start, end);
}

/**
* Checks if node is equal to passed node
*
* @param node - node to check
*/
public isEqual(node: InlineNode): node is FormattingInlineNode {
if (!(node instanceof FormattingInlineNode)) {
return false;
}

if (this.tool !== node.tool) {
return false;
}

/**
* @todo check data equality
*/

return true;
}

/**
* Merges current node with passed node
*
* @param node - node to merge with
*/
public mergeWith(node: InlineNode): void {
if (!this.isEqual(node)) {
throw new Error('Can not merge unequal nodes');
}

/**
* @todo merge data
*/

node.children.forEach((child) => {
this.append(child);
});

node.remove();
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { InlineToolName } from './InlineToolName';
import { InlineToolData } from './InlineToolData';
import type { ChildNodeConstructorOptions, ParentNodeConstructorOptions } from '../../interfaces';
import type { ChildNodeConstructorOptions } from '../../mixins/ChildNode';
import type { ParentNodeConstructorOptions } from '../../mixins/ParentNode';

export interface FormattingNodeConstructorParameters extends ChildNodeConstructorOptions, ParentNodeConstructorOptions {
export interface FormattingInlineNodeConstructorParameters extends ChildNodeConstructorOptions, ParentNodeConstructorOptions {
/**
* The name of the formatting tool applied to the content
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create, Nominal } from '../../../utils/Nominal';
import { create, Nominal } from '../../../../utils/Nominal';

/**
* Base type for Inline Tool data
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create, Nominal } from '../../../utils/Nominal';
import { create, Nominal } from '../../../../utils/Nominal';

/**
* Base type of the formatting node tool field
Expand All @@ -11,6 +11,6 @@ type InlineToolNameBase = string;
export type InlineToolName = Nominal<InlineToolNameBase, 'InlineToolName'>;

/**
* Function returns a value with the nominal FormattingNodeName type
* Function returns a value with the nominal InlineToolName type
*/
export const createInlineToolName = create<InlineToolName>();
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { FormattingNodeConstructorParameters } from './FormattingNodeConstructorParameters';
export { FormattingInlineNodeConstructorParameters } from './FormattingInlineNodeConstructorParameters';
export { InlineToolName, createInlineToolName } from './InlineToolName';
export { InlineToolData, createInlineToolData } from './InlineToolData';
Loading
Loading