diff --git a/.gitignore b/.gitignore index 854a697d..4a91a287 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ reports/ # stryker temp files .stryker-tmp +*.tsbuildinfo diff --git a/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts b/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts index a28a5986..cd7e9bfb 100644 --- a/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts +++ b/packages/model/src/entities/BlockNode/BlockNode.integration.spec.ts @@ -1,6 +1,7 @@ import { BlockNode, createDataKey } from './index.js'; import { BlockChildType } from './types/index.js'; import { NODE_TYPE_HIDDEN_PROP } from './consts.js'; +import { ValueNode } from '../ValueNode/index.js'; describe('BlockNode integration tests', () => { it('should create ValueNode by primitive value', () => { @@ -100,4 +101,29 @@ describe('BlockNode integration tests', () => { ], }); }); + + describe('.data', () => { + it('should return the data associated with this block node', () => { + // Arrange + const initData = { + key: 'value', + }; + const blockNode = new BlockNode({ + name: 'blockNode', + data: initData, + }); + + // Act + const data = blockNode.data; + + // Assert + expect(data).toHaveProperty('key'); + + const valueNode = (data as {key: ValueNode}).key; + + expect(valueNode).toBeInstanceOf(ValueNode); + expect(valueNode.serialized) + .toEqual(initData.key); + }); + }); }); diff --git a/packages/model/src/entities/BlockNode/BlockNode.spec.ts b/packages/model/src/entities/BlockNode/BlockNode.spec.ts index 725da31d..0309015b 100644 --- a/packages/model/src/entities/BlockNode/BlockNode.spec.ts +++ b/packages/model/src/entities/BlockNode/BlockNode.spec.ts @@ -372,6 +372,52 @@ describe('BlockNode', () => { }); }); + describe('.name', () => { + it('should return a name of a tool that created a BlockNode', () => { + const blockNodeName = createBlockToolName('paragraph'); + + const blockNode = new BlockNode({ + name: blockNodeName, + data: {}, + parent: {} as EditorDocument, + }); + + expect(blockNode.name) + .toEqual(blockNodeName); + }); + }); + + describe('.tunes', () => { + it('should return an object with tunes associated with the BlockNode', () => { + const blockTunesNames = [ + 'align' as BlockTuneName, + 'font-size' as BlockTuneName, + 'font-weight' as BlockTuneName, + ]; + + const blockTunes = blockTunesNames.reduce((acc, name) => ({ + ...acc, + [name]: {}, + }), {}); + + const blockNode = new BlockNode({ + name: createBlockToolName('paragraph'), + data: {}, + parent: {} as EditorDocument, + tunes: blockTunes, + }); + + const tunes = Object.entries(blockNode.tunes); + + tunes.forEach(([name, tune]) => { + expect(name) + .toEqual(createBlockTuneName(name)); + expect(tune) + .toBeInstanceOf(BlockTune); + }); + }); + }); + describe('.updateTuneData()', () => { afterEach(() => { jest.clearAllMocks(); diff --git a/packages/model/src/entities/BlockNode/index.ts b/packages/model/src/entities/BlockNode/index.ts index 5367973a..572d52cb 100644 --- a/packages/model/src/entities/BlockNode/index.ts +++ b/packages/model/src/entities/BlockNode/index.ts @@ -23,6 +23,7 @@ import { TextNode } from '../inline-fragments/index.js'; import { get, has } from '../../utils/keypath.js'; import { NODE_TYPE_HIDDEN_PROP } from './consts.js'; import { mapObject } from '../../utils/mapObject.js'; +import type { DeepReadonly } from '../../utils/DeepReadonly'; import { EventBus } from '../../utils/EventBus/EventBus.js'; import { EventType } from '../../utils/EventBus/types/EventType.js'; import { @@ -97,18 +98,17 @@ export class BlockNode extends EventBus { } /** - * Getter to access BlockNode data + * Allows accessing Block name */ - public get data(): Readonly { - return this.#data; + public get name(): string { + return this.#name; } - /** - * Getter to access BlockNode data + * Allows accessing Block data */ - public get tunes(): Readonly> { - return this.#tunes; + public get data(): DeepReadonly { + return this.#data; } /** @@ -118,6 +118,13 @@ export class BlockNode extends EventBus { return this.#parent; } + /** + * Getter to access BlockNode data + */ + public get tunes(): Readonly> { + return this.#tunes; + } + /** * Returns serialized object representing the BlockNode */ diff --git a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts index 45944b89..cd5ab64e 100644 --- a/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts +++ b/packages/model/src/entities/EditorDocument/EditorDocument.spec.ts @@ -65,6 +65,38 @@ describe('EditorDocument', () => { }); }); + describe('.children', () => { + it('should return an array of the blocks of the document', () => { + // Arrange + const blocksCount = 3; + const document = new EditorDocument({ + properties: { + readOnly: false, + }, + }); + + for (let i = 0; i < blocksCount; i++) { + const blockData = { + name: 'header' as BlockToolName, + }; + + document.addBlock(blockData); + } + + // Act + const actual = document.children; + + // Assert + expect(actual) + .toHaveLength(blocksCount); + + actual.forEach((block) => { + expect(block) + .toBeInstanceOf(BlockNode); + }); + }); + }); + describe('.addBlock()', () => { it('should add the block to the end of the document if index is not provided', () => { const document = createEditorDocumentWithSomeBlocks(); diff --git a/packages/model/src/entities/EditorDocument/index.ts b/packages/model/src/entities/EditorDocument/index.ts index c391a45d..4ddf63cb 100644 --- a/packages/model/src/entities/EditorDocument/index.ts +++ b/packages/model/src/entities/EditorDocument/index.ts @@ -6,6 +6,7 @@ import type { InlineToolData, InlineToolName } from '../inline-fragments'; import { IoCContainer, TOOLS_REGISTRY } from '../../IoC/index.js'; import { ToolsRegistry } from '../../tools/index.js'; import type { BlockNodeSerialized } from '../BlockNode/types'; +import type { DeepReadonly } from '../../utils/DeepReadonly'; import { EventBus } from '../../utils/EventBus/EventBus.js'; import { EventType } from '../../utils/EventBus/types/EventType.js'; import type { @@ -60,6 +61,13 @@ export class EditorDocument extends EventBus { this.#initialize(blocks); } + /** + * Allows accessing Document child nodes + */ + public get children(): ReadonlyArray> { + return this.#children; + } + /** * Returns count of child BlockNodes of the EditorDocument. */ diff --git a/packages/model/src/entities/ValueNode/ValueNode.spec.ts b/packages/model/src/entities/ValueNode/ValueNode.spec.ts index c39cda37..f5e0c4bd 100644 --- a/packages/model/src/entities/ValueNode/ValueNode.spec.ts +++ b/packages/model/src/entities/ValueNode/ValueNode.spec.ts @@ -86,4 +86,20 @@ describe('ValueNode', () => { expect(serializedValue).toHaveProperty(NODE_TYPE_HIDDEN_PROP, BlockChildType.Value); }); }); + + describe('.value', () => { + it('should return the value associated with this value node', () => { + // Arrange + const longitude = 23.123; + const longitudeValueNode = new ValueNode({ + value: longitude, + }); + + // Act + const serializedLongitude = longitudeValueNode.value; + + // Assert + expect(serializedLongitude).toStrictEqual(longitude); + }); + }); }); diff --git a/packages/model/src/entities/ValueNode/index.ts b/packages/model/src/entities/ValueNode/index.ts index ed08266e..0608a78c 100644 --- a/packages/model/src/entities/ValueNode/index.ts +++ b/packages/model/src/entities/ValueNode/index.ts @@ -57,6 +57,13 @@ export class ValueNode extends EventBus { return value as ValueSerialized; } + + /** + * Returns the data associated with this value node. + */ + public get value(): Readonly { + return this.#value; + } } export type { diff --git a/packages/model/src/utils/DeepReadonly.ts b/packages/model/src/utils/DeepReadonly.ts new file mode 100644 index 00000000..a664b5c2 --- /dev/null +++ b/packages/model/src/utils/DeepReadonly.ts @@ -0,0 +1,14 @@ +/** + * Make all properties in T readonly + */ +export type DeepReadonly = + T extends (infer R)[] ? DeepReadonlyArray : + T extends (...args: unknown[]) => unknown ? T : + T extends object ? DeepReadonlyObject : + T; + +interface DeepReadonlyArray extends ReadonlyArray> {} + +type DeepReadonlyObject = { + readonly [P in keyof T]: DeepReadonly; +}; diff --git a/packages/model/tsconfig.json b/packages/model/tsconfig.json index deac9a70..f9b22ef7 100644 --- a/packages/model/tsconfig.json +++ b/packages/model/tsconfig.json @@ -8,7 +8,9 @@ "strict": true, "skipLibCheck": true, "experimentalDecorators": true, - "types": ["jest"] + "types": ["jest"], + "composite": true, + "rootDir": "src/" }, "include": ["src/**/*"], "exclude": [ diff --git a/packages/playground/.eslintrc.cjs b/packages/playground/.eslintrc.cjs index 3ebefd89..7c4ace9f 100644 --- a/packages/playground/.eslintrc.cjs +++ b/packages/playground/.eslintrc.cjs @@ -16,5 +16,9 @@ module.exports = { }, parser: 'vue-eslint-parser', rules: { + 'jsdoc/require-returns': 'off', + 'jsdoc/require-param-type': 'off', + 'vue/multi-word-component-names': 'off', + }, }; diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index 360a595f..be5f1897 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -1,7 +1,9 @@ diff --git a/packages/playground/src/components/Indent.vue b/packages/playground/src/components/Indent.vue new file mode 100644 index 00000000..be8069cc --- /dev/null +++ b/packages/playground/src/components/Indent.vue @@ -0,0 +1,11 @@ + + + diff --git a/packages/playground/src/components/Node.vue b/packages/playground/src/components/Node.vue new file mode 100644 index 00000000..0131553c --- /dev/null +++ b/packages/playground/src/components/Node.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/playground/src/components/Value.vue b/packages/playground/src/components/Value.vue new file mode 100644 index 00000000..d02d0a86 --- /dev/null +++ b/packages/playground/src/components/Value.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/playground/src/components/index.ts b/packages/playground/src/components/index.ts new file mode 100644 index 00000000..0441128a --- /dev/null +++ b/packages/playground/src/components/index.ts @@ -0,0 +1,9 @@ +import Node from './Node.vue'; +import Indent from './Indent.vue'; +import Value from './Value.vue'; + +export { + Node, + Indent, + Value +}; diff --git a/packages/playground/src/style.css b/packages/playground/src/style.css index 7294765e..f328f1a7 100644 --- a/packages/playground/src/style.css +++ b/packages/playground/src/style.css @@ -1,11 +1,18 @@ :root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; + --family: system-ui, Inter, Avenir, Helvetica, Arial, sans-serif; + --rounded-family: SF Pro Rounded, "ui-rounded", system-ui; + + --background: #242424; + --foreground: rgba(255, 255, 255, 0.87); + + font-family: var(--family); font-weight: 400; + font-size: 14px; + + color-scheme: dark; - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; + background-color: var(--background); + color: var(--foreground); font-synthesis: none; text-rendering: optimizeLegibility; @@ -14,67 +21,23 @@ -webkit-text-size-adjust: 100%; } -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - body { margin: 0; display: flex; - place-items: center; min-width: 320px; min-height: 100vh; } -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -.card { - padding: 2em; -} - #app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} + display: grid; + grid-template-columns: 100%; + grid-template-rows: 1fr; + min-height: 100%; + width: 100%; -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; } diff --git a/packages/playground/src/utils/isObject.ts b/packages/playground/src/utils/isObject.ts new file mode 100644 index 00000000..805fa82b --- /dev/null +++ b/packages/playground/src/utils/isObject.ts @@ -0,0 +1,8 @@ +/** + * Returns whether the value is an object + * + * @param value - The value to check + */ +export function isObject(value: unknown): value is object { + return Array.isArray(value) === false && typeof value === 'object' && value !== null; +} diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json index 22431f1a..fc9a5e02 100644 --- a/packages/playground/tsconfig.json +++ b/packages/playground/tsconfig.json @@ -18,7 +18,11 @@ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "paths": { + "@/*": ["./src/*"], + }, + "baseUrl": ".", }, "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], "references": [ diff --git a/packages/playground/vite.config.ts b/packages/playground/vite.config.ts index 05c17402..2c843219 100644 --- a/packages/playground/vite.config.ts +++ b/packages/playground/vite.config.ts @@ -1,7 +1,14 @@ import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' +import path from 'node:path'; + // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, })