From 25d0ef5f1a08b6d998eb351e6ac8784202de7ee0 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 13:35:34 +0300 Subject: [PATCH 01/13] Refactor Piece component to utilize useMark hook for child rendering - Replaced direct child rendering logic in Piece with useMark hook to manage children as ReactNode. - Introduced useMark hook to encapsulate mark handling, including depth, parent, and child token management. - Updated related tests and documentation to reflect changes in child handling and mark properties. - Adjusted Storybook examples to use updated mark properties for consistency. --- packages/markput/src/components/Piece.tsx | 29 ++++---------- .../utils/hooks/{useMark.ts => useMark.tsx} | 23 +++++++++-- .../src/pages/Nested/Nested.stories.tsx | 4 +- .../src/pages/Nested/nested.spec.tsx | 2 +- .../src/content/docs/api/functions/useMark.md | 2 +- .../docs/api/interfaces/MarkHandler.md | 40 ++++++++++++------- 6 files changed, 57 insertions(+), 43 deletions(-) rename packages/markput/src/utils/hooks/{useMark.ts => useMark.tsx} (87%) diff --git a/packages/markput/src/components/Piece.tsx b/packages/markput/src/components/Piece.tsx index 599cfe1e..f64d321f 100644 --- a/packages/markput/src/components/Piece.tsx +++ b/packages/markput/src/components/Piece.tsx @@ -1,9 +1,8 @@ -import type {ReactNode} from 'react' import {useStore} from '../utils/hooks/useStore' import {useSlot} from '../utils/hooks/useSlot' import {useToken} from '../utils/providers/TokenProvider' -// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token renders Piece, Piece renders Token for children -import {Token} from './Token' +// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token renders Piece, Piece uses useMark which renders Token for children +import {useMark} from '../utils/hooks/useMark' import type {MarkProps} from '../types' /** @@ -11,12 +10,12 @@ import type {MarkProps} from '../types' * * This component: * 1. Retrieves the MarkToken from context - * 2. Constructs MarkProps (value, meta, nested, children) - * 3. Recursively renders nested children if present + * 2. Uses useMark hook to get children ReactNode + * 3. Constructs MarkProps (value, meta, nested, children) * 4. Resolves Mark component and props using useSlot hook * 5. Passes result to the resolved Mark component * - * Children rendering: + * Children rendering is handled by useMark hook: * - If token.children is empty: children prop is undefined (backward compatible) * - If token.children has items: recursively renders them as ReactNode * @@ -26,13 +25,8 @@ import type {MarkProps} from '../types' */ export function Piece() { const node = useToken() - const {options, key} = useStore( - store => ({ - options: store.props.options, - key: store.key, - }), - true - ) + const {options} = useStore(store => ({options: store.props.options}), true) + const mark = useMark() // Ensure it's a MarkToken if (node.type !== 'mark') { @@ -42,18 +36,11 @@ export function Piece() { // Get option and construct base MarkProps const option = options?.[node.descriptor.index] - // Construct children ReactNode from token.children if present - // Nested tokens render as non-editable content (isNested=true) - const children: ReactNode | undefined = - node.children.length > 0 - ? node.children.map(child => ) - : undefined - const markPropsData: MarkProps = { value: node.value, meta: node.meta, nested: node.nested?.content, - children, + children: mark.children, } // Resolve Mark component and props with proper fallback chain diff --git a/packages/markput/src/utils/hooks/useMark.ts b/packages/markput/src/utils/hooks/useMark.tsx similarity index 87% rename from packages/markput/src/utils/hooks/useMark.ts rename to packages/markput/src/utils/hooks/useMark.tsx index d8c2fc98..0ce6e9a6 100644 --- a/packages/markput/src/utils/hooks/useMark.ts +++ b/packages/markput/src/utils/hooks/useMark.tsx @@ -1,9 +1,11 @@ -import type {RefObject} from 'react' +import type {ReactNode, RefObject} from 'react' import {useEffect, useMemo, useRef, useState} from 'react' import type {MarkToken, Store, Token} from '@markput/core' import {SystemEvent} from '@markput/core' import {useToken} from '../providers/TokenProvider' import {useStore} from './useStore' +// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: useMark renders Token for children +import {Token as TokenComponent} from '../../components/Token' interface MarkStruct { label: string @@ -48,7 +50,11 @@ export interface MarkHandler extends MarkStruct { /** * Array of child tokens (read-only) */ - children: Token[] + tokens: Token[] + /** + * Rendered children as ReactNode + */ + children: ReactNode } export interface MarkOptions { @@ -82,11 +88,19 @@ export const useMark = (options: MarkOption const depth = useMemo(() => calculateDepth(token, store.tokens), [token, store.tokens]) const parent = useMemo(() => findParent(token, store.tokens), [token, store.tokens]) + // Compute children ReactNode from token.children if present + // Nested tokens render as non-editable content (isNested=true) + const childrenReactNode: ReactNode = + token.children.length > 0 + ? token.children.map(child => ) + : undefined + // Extend mark with tree navigation properties mark.depth = depth mark.hasChildren = token.children.length > 0 mark.parent = parent - mark.children = token.children + mark.tokens = token.children + mark.children = childrenReactNode return mark } @@ -170,7 +184,8 @@ export class MarkHandlerP { depth: number = 0 hasChildren: boolean = false parent?: MarkToken - children: Token[] = [] + tokens: Token[] = [] + children: ReactNode = undefined get label() { return this.#token.content diff --git a/packages/storybook/src/pages/Nested/Nested.stories.tsx b/packages/storybook/src/pages/Nested/Nested.stories.tsx index efde0596..1710b69d 100644 --- a/packages/storybook/src/pages/Nested/Nested.stories.tsx +++ b/packages/storybook/src/pages/Nested/Nested.stories.tsx @@ -191,7 +191,7 @@ const InteractiveMark = ({children, nested}: {value?: string; children?: ReactNo console.log('Mark clicked:', { depth: mark.depth, hasChildren: mark.hasChildren, - childrenCount: mark.children.length, + childrenCount: mark.tokens.length, parent: mark.parent ? 'has parent' : 'root level', }) }} @@ -207,7 +207,7 @@ const InteractiveMark = ({children, nested}: {value?: string; children?: ReactNo cursor: 'pointer', transition: 'all 0.2s', }} - title={`Depth: ${mark.depth}, Children: ${mark.children.length}`} + title={`Depth: ${mark.depth}, Children: ${mark.tokens.length}`} > {children || nested} diff --git a/packages/storybook/src/pages/Nested/nested.spec.tsx b/packages/storybook/src/pages/Nested/nested.spec.tsx index 060cfc84..9f876ad7 100644 --- a/packages/storybook/src/pages/Nested/nested.spec.tsx +++ b/packages/storybook/src/pages/Nested/nested.spec.tsx @@ -176,7 +176,7 @@ describe('Nested Marks Tree Navigation', () => { const ChildrenCountMark = ({children}: {value?: string; children?: ReactNode}) => { const mark = useMark() if (mark.depth === 0) { - capturedChildrenCount = mark.children.length + capturedChildrenCount = mark.tokens.length } return {children} } diff --git a/packages/website/src/content/docs/api/functions/useMark.md b/packages/website/src/content/docs/api/functions/useMark.md index ee81d9c4..10aa59d5 100644 --- a/packages/website/src/content/docs/api/functions/useMark.md +++ b/packages/website/src/content/docs/api/functions/useMark.md @@ -9,7 +9,7 @@ title: "useMark" function useMark(options): MarkHandler; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:62](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L62) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:68 ## Type Parameters diff --git a/packages/website/src/content/docs/api/interfaces/MarkHandler.md b/packages/website/src/content/docs/api/interfaces/MarkHandler.md index ce4aa806..39fb2c79 100644 --- a/packages/website/src/content/docs/api/interfaces/MarkHandler.md +++ b/packages/website/src/content/docs/api/interfaces/MarkHandler.md @@ -5,7 +5,7 @@ prev: false title: "MarkHandler" --- -Defined in: [packages/markput/src/utils/hooks/useMark.ts:13](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L13) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:15 ## Extends @@ -25,7 +25,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.ts:13](https://github.com/ change: (props, options?) => void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:23](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L23) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:25 Change mark. @@ -46,12 +46,12 @@ Change mark. ### children ```ts -children: Token[]; +children: ReactNode; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:51](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L51) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:57 -Array of child tokens (read-only) +Rendered children as ReactNode *** @@ -61,7 +61,7 @@ Array of child tokens (read-only) depth: number; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:39](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L39) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:41 Nesting depth of this mark (0 for root-level marks) @@ -73,7 +73,7 @@ Nesting depth of this mark (0 for root-level marks) hasChildren: boolean; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:43](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L43) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:45 Whether this mark has nested children @@ -85,7 +85,7 @@ Whether this mark has nested children label: string; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:9](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L9) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:11 #### Inherited from @@ -101,7 +101,7 @@ MarkStruct.label optional meta: string; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:35](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L35) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:37 Meta value of the mark @@ -113,7 +113,7 @@ Meta value of the mark optional parent: MarkToken; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:47](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L47) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:49 Parent mark token (undefined for root-level marks) @@ -125,7 +125,7 @@ Parent mark token (undefined for root-level marks) optional readOnly: boolean; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:31](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L31) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:33 Passed the readOnly prop value @@ -137,7 +137,7 @@ Passed the readOnly prop value ref: RefObject; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:17](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L17) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:19 MarkStruct ref. Used for focusing and key handling operations. @@ -149,7 +149,7 @@ MarkStruct ref. Used for focusing and key handling operations. remove: () => void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:27](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L27) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:29 Remove itself. @@ -159,13 +159,25 @@ Remove itself. *** +### tokens + +```ts +tokens: Token[]; +``` + +Defined in: packages/markput/src/utils/hooks/useMark.tsx:53 + +Array of child tokens (read-only) + +*** + ### value? ```ts optional value: string; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.ts:10](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L10) +Defined in: packages/markput/src/utils/hooks/useMark.tsx:12 #### Inherited from From ede41efc111226bdd11721f8f0e2361e57c1bd6e Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 14:13:26 +0300 Subject: [PATCH 02/13] Refactor Piece and Token components for improved type safety and circular dependency resolution - Updated Piece component to render children directly, eliminating the use of the useMark hook for child rendering, thus breaking the circular import. - Introduced MarkTokenComponent and TextTokenComponent in the Token component for better type discrimination and compile-time safety. - Enhanced MarkHandler class to compute depth, parent, and child tokens lazily, improving performance and clarity. - Updated documentation links in API references to reflect changes in the useMark hook and MarkHandler interface. - Adjusted related tests and Storybook examples to align with the new component structure and type safety improvements. --- packages/markput/src/components/Piece.tsx | 29 ++- packages/markput/src/components/Token.tsx | 83 ++++++-- packages/markput/src/utils/hooks/useMark.tsx | 199 +++++++++--------- .../src/content/docs/api/functions/useMark.md | 2 +- .../docs/api/interfaces/MarkHandler.md | 52 ++--- 5 files changed, 200 insertions(+), 165 deletions(-) diff --git a/packages/markput/src/components/Piece.tsx b/packages/markput/src/components/Piece.tsx index f64d321f..14b63d28 100644 --- a/packages/markput/src/components/Piece.tsx +++ b/packages/markput/src/components/Piece.tsx @@ -1,21 +1,26 @@ +import type {MarkToken} from '@markput/core' import {useStore} from '../utils/hooks/useStore' import {useSlot} from '../utils/hooks/useSlot' import {useToken} from '../utils/providers/TokenProvider' -// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token renders Piece, Piece uses useMark which renders Token for children -import {useMark} from '../utils/hooks/useMark' import type {MarkProps} from '../types' +// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Piece → Token → MarkTokenComponent → Piece +import {Token} from './Token' /** * Piece component - renders a MarkToken with its custom Mark component * * This component: - * 1. Retrieves the MarkToken from context - * 2. Uses useMark hook to get children ReactNode + * 1. Retrieves the MarkToken from context (type-safe via MarkTokenComponent) + * 2. Renders children directly (breaking circular import with useMark) * 3. Constructs MarkProps (value, meta, nested, children) * 4. Resolves Mark component and props using useSlot hook * 5. Passes result to the resolved Mark component * - * Children rendering is handled by useMark hook: + * Type safety: + * - Piece is only rendered via MarkTokenComponent which guarantees MarkToken type + * - Runtime check kept for defensive programming but should never trigger + * + * Children rendering: * - If token.children is empty: children prop is undefined (backward compatible) * - If token.children has items: recursively renders them as ReactNode * @@ -25,10 +30,10 @@ import type {MarkProps} from '../types' */ export function Piece() { const node = useToken() - const {options} = useStore(store => ({options: store.props.options}), true) - const mark = useMark() + const {options, key} = useStore(store => ({options: store.props.options, key: store.key}), true) - // Ensure it's a MarkToken + // Type guard - should never trigger since Piece is only rendered via MarkTokenComponent + // Kept for defensive programming and to satisfy TypeScript if (node.type !== 'mark') { throw new Error('Piece component expects a MarkToken') } @@ -36,11 +41,17 @@ export function Piece() { // Get option and construct base MarkProps const option = options?.[node.descriptor.index] + // Render children directly in component (not in hook) to break circular import + const children = + node.children.length > 0 + ? node.children.map(child => ) + : undefined + const markPropsData: MarkProps = { value: node.value, meta: node.meta, nested: node.nested?.content, - children: mark.children, + children, } // Resolve Mark component and props with proper fallback chain diff --git a/packages/markput/src/components/Token.tsx b/packages/markput/src/components/Token.tsx index 185cbccd..b9d33861 100644 --- a/packages/markput/src/components/Token.tsx +++ b/packages/markput/src/components/Token.tsx @@ -1,35 +1,80 @@ import {memo} from 'react' -import type {Token as TokenType} from '@markput/core' +import type {MarkToken, TextToken, Token as TokenType} from '@markput/core' import {TokenProvider} from '../utils/providers/TokenProvider' -// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token renders Piece, Piece renders Token for children +// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token → Piece → Token import {Piece} from './Piece' import {TextSpan} from './TextSpan' /** * Token component - renders a single token (text or mark) with recursive support for nested marks * - * This component handles both TextToken and MarkToken types: - * - TextToken: renders as TextSpan (editable text) when isNested=false, or plain text when isNested=true - * - MarkToken: renders as Piece (custom Mark component with optional nested children) + * This component discriminates token types and delegates to type-safe sub-components: + * - MarkToken → MarkTokenComponent (renders Piece with custom Mark component) + * - TextToken → TextTokenComponent (renders TextSpan or plain text) * - * The isNested prop determines editing behavior: + * Type discrimination happens here at the top level, allowing sub-components + * to have compile-time type safety without runtime checks. + * + * The isNested prop determines editing behavior for TextTokens: * - isNested=false (default): TextTokens are editable contentEditable spans * - isNested=true: TextTokens render as plain text within nested marks * - * The component is memoized for performance and provides the token via context - * to child components through TokenProvider. + * The component is memoized for performance. */ -export const Token = memo(({mark, isNested = false}: {mark: TokenType; isNested?: boolean}) => ( - - {mark.type === 'mark' ? ( - - ) : isNested ? ( - // For nested text tokens, render as plain text without contentEditable - mark.content - ) : ( - - )} +export const Token = memo(({mark, isNested = false}: {mark: TokenType; isNested?: boolean}) => { + // Type discrimination at top level for compile-time safety in sub-components + if (mark.type === 'mark') { + return + } + + return +}) + +Token.displayName = 'Token' + +/** + * MarkTokenComponent - renders a MarkToken with type safety + * + * This component: + * - Accepts only MarkToken type (compile-time safety) + * - Wraps content in TokenProvider for context access + * - Delegates rendering to Piece component + * + * The component is memoized for performance. + */ +const MarkTokenComponent = memo(({token}: {token: MarkToken}) => ( + + )) -Token.displayName = 'Token' +MarkTokenComponent.displayName = 'MarkTokenComponent' + +/** + * TextTokenComponent - renders a TextToken with type safety + * + * This component: + * - Accepts only TextToken type (compile-time safety) + * - isNested=true: renders plain text (no context needed) + * - isNested=false: wraps in TokenProvider and renders TextSpan + * + * Performance optimization: + * - Nested text tokens skip TokenProvider since context is never consumed + * + * The component is memoized for performance. + */ +const TextTokenComponent = memo(({token, isNested = false}: {token: TextToken; isNested?: boolean}) => { + // Optimization: nested text tokens don't need context provider + // They render as plain text and no child component consumes the token + if (isNested) { + return <>{token.content} + } + + return ( + + + + ) +}) + +TextTokenComponent.displayName = 'TextTokenComponent' diff --git a/packages/markput/src/utils/hooks/useMark.tsx b/packages/markput/src/utils/hooks/useMark.tsx index 0ce6e9a6..39d279a9 100644 --- a/packages/markput/src/utils/hooks/useMark.tsx +++ b/packages/markput/src/utils/hooks/useMark.tsx @@ -1,11 +1,9 @@ -import type {ReactNode, RefObject} from 'react' -import {useEffect, useMemo, useRef, useState} from 'react' +import type {RefObject} from 'react' +import {useEffect, useRef, useState} from 'react' import type {MarkToken, Store, Token} from '@markput/core' import {SystemEvent} from '@markput/core' import {useToken} from '../providers/TokenProvider' import {useStore} from './useStore' -// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: useMark renders Token for children -import {Token as TokenComponent} from '../../components/Token' interface MarkStruct { label: string @@ -36,25 +34,23 @@ export interface MarkHandler extends MarkStruct { */ meta?: string /** - * Nesting depth of this mark (0 for root-level marks) + * Nesting depth of this mark (0 for root-level marks). + * Computed lazily on access. */ - depth: number + readonly depth: number /** * Whether this mark has nested children */ - hasChildren: boolean + readonly hasChildren: boolean /** - * Parent mark token (undefined for root-level marks) + * Parent mark token (undefined for root-level marks). + * Computed lazily on access. */ - parent?: MarkToken + readonly parent: MarkToken | undefined /** * Array of child tokens (read-only) */ - tokens: Token[] - /** - * Rendered children as ReactNode - */ - children: ReactNode + readonly tokens: Token[] } export interface MarkOptions { @@ -84,93 +80,9 @@ export const useMark = (options: MarkOption mark.readOnly = readOnly }, [readOnly]) - // Calculate tree navigation properties - const depth = useMemo(() => calculateDepth(token, store.tokens), [token, store.tokens]) - const parent = useMemo(() => findParent(token, store.tokens), [token, store.tokens]) - - // Compute children ReactNode from token.children if present - // Nested tokens render as non-editable content (isNested=true) - const childrenReactNode: ReactNode = - token.children.length > 0 - ? token.children.map(child => ) - : undefined - - // Extend mark with tree navigation properties - mark.depth = depth - mark.hasChildren = token.children.length > 0 - mark.parent = parent - mark.tokens = token.children - mark.children = childrenReactNode - return mark } -/** - * Calculate the nesting depth of a token in the tree - * @param token - The token to calculate depth for - * @param tokens - Root-level tokens array - * @returns Depth (0 for root-level, 1+ for nested) - */ -function calculateDepth(token: MarkToken, tokens: Token[]): number { - let depth = 0 - const visited = new Set() - - function findDepthRecursive(currentTokens: Token[], currentDepth: number): boolean { - for (const t of currentTokens) { - if (visited.has(t)) continue - visited.add(t) - - if (t === token) { - depth = currentDepth - return true - } - - if (t.type === 'mark' && t.children.length > 0) { - if (findDepthRecursive(t.children, currentDepth + 1)) { - return true - } - } - } - return false - } - - findDepthRecursive(tokens, 0) - return depth -} - -/** - * Find the parent MarkToken of a given token - * @param token - The token to find parent for - * @param tokens - Root-level tokens array - * @returns Parent MarkToken or undefined if at root level - */ -function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { - let parent: MarkToken | undefined - const visited = new Set() - - function findParentRecursive(currentTokens: Token[], currentParent?: MarkToken): boolean { - for (const t of currentTokens) { - if (visited.has(t)) continue - visited.add(t) - - if (t === token) { - parent = currentParent - return true - } - - if (t.type === 'mark' && t.children.length > 0) { - if (findParentRecursive(t.children, t)) { - return true - } - } - } - return false - } - - findParentRecursive(tokens) - return parent -} - type MarkHandlerPConstruct = {ref: RefObject; options: MarkOptions; store: Store; token: MarkToken} export class MarkHandlerP { @@ -180,13 +92,6 @@ export class MarkHandlerP { readOnly?: boolean - // Tree navigation properties (set by useMark hook) - depth: number = 0 - hasChildren: boolean = false - parent?: MarkToken - tokens: Token[] = [] - children: ReactNode = undefined - get label() { return this.#token.content } @@ -224,6 +129,36 @@ export class MarkHandlerP { this.#store.bus.send(SystemEvent.Change, {node: this.#token}) } + /** + * Nesting depth of this mark (0 for root-level marks). + * Computed lazily on access - O(n) traversal. + */ + get depth(): number { + return calculateDepth(this.#token, this.#store.tokens) + } + + /** + * Whether this mark has nested children + */ + get hasChildren(): boolean { + return this.#token.children.length > 0 + } + + /** + * Parent mark token (undefined for root-level marks). + * Computed lazily on access - O(n) traversal. + */ + get parent(): MarkToken | undefined { + return findParent(this.#token, this.#store.tokens) + } + + /** + * Array of child tokens (read-only) + */ + get tokens(): Token[] { + return this.#token.children + } + change = (props: MarkStruct) => { this.#token.content = props.label this.#token.value = props.value ?? '' @@ -233,6 +168,60 @@ export class MarkHandlerP { remove = () => this.#store.bus.send(SystemEvent.Delete, {token: this.#token}) } +/** + * Calculate the nesting depth of a token in the tree. + * O(n) traversal - only called when depth is accessed. + */ +function calculateDepth(token: MarkToken, tokens: Token[]): number { + let depth = 0 + + function findDepthRecursive(currentTokens: Token[], currentDepth: number): boolean { + for (const t of currentTokens) { + if (t === token) { + depth = currentDepth + return true + } + + if (t.type === 'mark' && t.children.length > 0) { + if (findDepthRecursive(t.children, currentDepth + 1)) { + return true + } + } + } + return false + } + + findDepthRecursive(tokens, 0) + return depth +} + +/** + * Find the parent MarkToken of a given token. + * O(n) traversal - only called when parent is accessed. + */ +function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { + let parent: MarkToken | undefined + + function findParentRecursive(currentTokens: Token[], currentParent?: MarkToken): boolean { + for (const t of currentTokens) { + if (t === token) { + parent = currentParent + return true + } + + if (t.type === 'mark' && t.children.length > 0) { + if (findParentRecursive(t.children, t)) { + return true + } + } + } + return false + } + + findParentRecursive(tokens) + return parent +} + function useUncontrolledInit(ref: RefObject, options: MarkOptions, token: MarkToken) { useEffect(() => { if (ref.current && !options.controlled) ref.current.textContent = token.content diff --git a/packages/website/src/content/docs/api/functions/useMark.md b/packages/website/src/content/docs/api/functions/useMark.md index 10aa59d5..42bb0d79 100644 --- a/packages/website/src/content/docs/api/functions/useMark.md +++ b/packages/website/src/content/docs/api/functions/useMark.md @@ -9,7 +9,7 @@ title: "useMark" function useMark(options): MarkHandler; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:68 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:64](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L64) ## Type Parameters diff --git a/packages/website/src/content/docs/api/interfaces/MarkHandler.md b/packages/website/src/content/docs/api/interfaces/MarkHandler.md index 39fb2c79..29c06308 100644 --- a/packages/website/src/content/docs/api/interfaces/MarkHandler.md +++ b/packages/website/src/content/docs/api/interfaces/MarkHandler.md @@ -5,7 +5,7 @@ prev: false title: "MarkHandler" --- -Defined in: packages/markput/src/utils/hooks/useMark.tsx:15 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:13](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L13) ## Extends @@ -25,7 +25,7 @@ Defined in: packages/markput/src/utils/hooks/useMark.tsx:15 change: (props, options?) => void; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:25 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:23](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L23) Change mark. @@ -43,37 +43,26 @@ Change mark. *** -### children - -```ts -children: ReactNode; -``` - -Defined in: packages/markput/src/utils/hooks/useMark.tsx:57 - -Rendered children as ReactNode - -*** - ### depth ```ts -depth: number; +readonly depth: number; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:41 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:40](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L40) -Nesting depth of this mark (0 for root-level marks) +Nesting depth of this mark (0 for root-level marks). +Computed lazily on access. *** ### hasChildren ```ts -hasChildren: boolean; +readonly hasChildren: boolean; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:45 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:44](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L44) Whether this mark has nested children @@ -85,7 +74,7 @@ Whether this mark has nested children label: string; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:11 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:9](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L9) #### Inherited from @@ -101,21 +90,22 @@ MarkStruct.label optional meta: string; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:37 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:35](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L35) Meta value of the mark *** -### parent? +### parent ```ts -optional parent: MarkToken; +readonly parent: MarkToken | undefined; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:49 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:49](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L49) -Parent mark token (undefined for root-level marks) +Parent mark token (undefined for root-level marks). +Computed lazily on access. *** @@ -125,7 +115,7 @@ Parent mark token (undefined for root-level marks) optional readOnly: boolean; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:33 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:31](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L31) Passed the readOnly prop value @@ -137,7 +127,7 @@ Passed the readOnly prop value ref: RefObject; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:19 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:17](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L17) MarkStruct ref. Used for focusing and key handling operations. @@ -149,7 +139,7 @@ MarkStruct ref. Used for focusing and key handling operations. remove: () => void; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:29 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:27](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L27) Remove itself. @@ -162,10 +152,10 @@ Remove itself. ### tokens ```ts -tokens: Token[]; +readonly tokens: Token[]; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:53 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:53](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L53) Array of child tokens (read-only) @@ -177,7 +167,7 @@ Array of child tokens (read-only) optional value: string; ``` -Defined in: packages/markput/src/utils/hooks/useMark.tsx:12 +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:10](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L10) #### Inherited from From bc91ae6d351dd9e7886e4e7698fdf92c0a52116c Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 14:38:25 +0300 Subject: [PATCH 03/13] Refactor useMark hook and related components for improved clarity and functionality - Simplified the useListener implementation in the Container component for better readability. - Refactored the MarkHandler class to enhance the management of mark properties, including the introduction of content as a key property instead of label. - Updated the change method to accept a more structured props object, allowing for simultaneous updates to content, value, and meta. - Adjusted related documentation to reflect changes in the MarkHandler interface and its properties. - Modified tests to ensure compatibility with the updated MarkHandler structure and behavior. --- packages/markput/src/components/Container.tsx | 8 +- packages/markput/src/utils/hooks/useMark.tsx | 94 +++----- .../src/pages/Nested/nested.spec.tsx | 2 +- .../src/content/docs/api/functions/useMark.md | 2 +- .../docs/api/interfaces/MarkHandler.md | 211 ++++++++++++------ 5 files changed, 180 insertions(+), 137 deletions(-) diff --git a/packages/markput/src/components/Container.tsx b/packages/markput/src/components/Container.tsx index 741f7443..647ee99a 100644 --- a/packages/markput/src/components/Container.tsx +++ b/packages/markput/src/components/Container.tsx @@ -20,13 +20,7 @@ export const Container = memo(() => { true ) - useListener( - 'input', - () => { - bus.send(SystemEvent.Change) - }, - [] - ) + useListener('input',() => bus.send(SystemEvent.Change),[]) return ( diff --git a/packages/markput/src/utils/hooks/useMark.tsx b/packages/markput/src/utils/hooks/useMark.tsx index 39d279a9..c89640b6 100644 --- a/packages/markput/src/utils/hooks/useMark.tsx +++ b/packages/markput/src/utils/hooks/useMark.tsx @@ -5,54 +5,6 @@ import {SystemEvent} from '@markput/core' import {useToken} from '../providers/TokenProvider' import {useStore} from './useStore' -interface MarkStruct { - label: string - value?: string -} - -export interface MarkHandler extends MarkStruct { - /** - * MarkStruct ref. Used for focusing and key handling operations. - */ - ref: RefObject - /** - * Change mark. - * @param {Object} options - The options object - * @param {boolean} options.silent - If true, doesn't change itself label and value, only pass change event. - */ - change: (props: MarkStruct, options?: {silent: boolean}) => void - /** - * Remove itself. - */ - remove: () => void - /** - * Passed the readOnly prop value - */ - readOnly?: boolean - /** - * Meta value of the mark - */ - meta?: string - /** - * Nesting depth of this mark (0 for root-level marks). - * Computed lazily on access. - */ - readonly depth: number - /** - * Whether this mark has nested children - */ - readonly hasChildren: boolean - /** - * Parent mark token (undefined for root-level marks). - * Computed lazily on access. - */ - readonly parent: MarkToken | undefined - /** - * Array of child tokens (read-only) - */ - readonly tokens: Token[] -} - export interface MarkOptions { /** * @default false @@ -70,7 +22,7 @@ export const useMark = (options: MarkOption throw new Error('useMark can only be used with mark tokens') } - const [mark] = useState(() => new MarkHandlerP({ref, store, options, token})) + const [mark] = useState(() => new MarkHandler({ref, store, token})) useUncontrolledInit(ref, options, token) @@ -83,30 +35,34 @@ export const useMark = (options: MarkOption return mark } -type MarkHandlerPConstruct = {ref: RefObject; options: MarkOptions; store: Store; token: MarkToken} - -export class MarkHandlerP { - ref: RefObject +export class MarkHandler { + readonly ref: RefObject readonly #store: Store readonly #token: MarkToken readOnly?: boolean - get label() { + constructor(param: {ref: RefObject; store: Store; token: MarkToken}) { + this.ref = param.ref + this.#store = param.store + this.#token = param.token + } + + /** + * Content/label of the mark (displayed text) + */ + get content() { return this.#token.content } - set label(value: string) { + set content(value: string) { this.#token.content = value this.#store.bus.send(SystemEvent.Change, {node: this.#token}) } - constructor(param: MarkHandlerPConstruct) { - this.ref = param.ref - this.#store = param.store - this.#token = param.token - } - + /** + * Value of the mark (hidden data) + */ get value() { return this.#token.value } @@ -116,6 +72,9 @@ export class MarkHandlerP { this.#store.bus.send(SystemEvent.Change, {node: this.#token}) } + /** + * Meta value of the mark + */ get meta() { return this.#token.meta } @@ -159,12 +118,21 @@ export class MarkHandlerP { return this.#token.children } - change = (props: MarkStruct) => { - this.#token.content = props.label + /** + * Change mark content, value, and/or meta at once. + */ + change = (props: {content: string; value?: string; meta?: string}) => { + this.#token.content = props.content this.#token.value = props.value ?? '' + if (props.meta !== undefined) { + this.#token.meta = props.meta + } this.#store.bus.send(SystemEvent.Change, {node: this.#token}) } + /** + * Remove this mark. + */ remove = () => this.#store.bus.send(SystemEvent.Delete, {token: this.#token}) } diff --git a/packages/storybook/src/pages/Nested/nested.spec.tsx b/packages/storybook/src/pages/Nested/nested.spec.tsx index 9f876ad7..6d8eee32 100644 --- a/packages/storybook/src/pages/Nested/nested.spec.tsx +++ b/packages/storybook/src/pages/Nested/nested.spec.tsx @@ -62,7 +62,7 @@ describe('Nested Marks Rendering', () => { it('should render different markup types nested', async () => { const TagMark = ({children}: {value?: string; children?: ReactNode}) => { const mark = useMark() - const isTag = mark.label.startsWith('#') + const isTag = mark.content.startsWith('#') return ( {children} diff --git a/packages/website/src/content/docs/api/functions/useMark.md b/packages/website/src/content/docs/api/functions/useMark.md index 42bb0d79..91936067 100644 --- a/packages/website/src/content/docs/api/functions/useMark.md +++ b/packages/website/src/content/docs/api/functions/useMark.md @@ -9,7 +9,7 @@ title: "useMark" function useMark(options): MarkHandler; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:64](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L64) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:16](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L16) ## Type Parameters diff --git a/packages/website/src/content/docs/api/interfaces/MarkHandler.md b/packages/website/src/content/docs/api/interfaces/MarkHandler.md index 29c06308..ab1db5f4 100644 --- a/packages/website/src/content/docs/api/interfaces/MarkHandler.md +++ b/packages/website/src/content/docs/api/interfaces/MarkHandler.md @@ -5,39 +5,67 @@ prev: false title: "MarkHandler" --- -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:13](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L13) - -## Extends - -- `MarkStruct` +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:38](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L38) ## Type Parameters -| Type Parameter | -| ------ | -| `T` | +| Type Parameter | Default type | +| ------ | ------ | +| `T` *extends* `HTMLElement` | `HTMLElement` | ## Properties -### change() +### readOnly? ```ts -change: (props, options?) => void; +optional readOnly: boolean; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:23](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L23) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:43](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L43) -Change mark. +*** -#### Parameters +### ref + +```ts +readonly ref: RefObject; +``` -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `props` | `MarkStruct` | - | -| `options?` | \{ `silent`: `boolean`; \} | The options object | -| `options.silent?` | `boolean` | If true, doesn't change itself label and value, only pass change event. | +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:39](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L39) -#### Returns +## Accessors + +### content + +#### Get Signature + +```ts +get content(): string; +``` + +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:54](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L54) + +Content/label of the mark (displayed text) + +##### Returns + +`string` + +#### Set Signature + +```ts +set content(value): void; +``` + +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:58](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L58) + +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `value` | `string` | + +##### Returns `void` @@ -45,132 +73,185 @@ Change mark. ### depth +#### Get Signature + ```ts -readonly depth: number; +get depth(): number; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:40](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L40) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:95](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L95) Nesting depth of this mark (0 for root-level marks). -Computed lazily on access. +Computed lazily on access - O(n) traversal. + +##### Returns + +`number` *** ### hasChildren +#### Get Signature + ```ts -readonly hasChildren: boolean; +get hasChildren(): boolean; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:44](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L44) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:102](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L102) Whether this mark has nested children +##### Returns + +`boolean` + *** -### label +### meta + +#### Get Signature ```ts -label: string; +get meta(): string | undefined; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:9](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L9) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:78](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L78) -#### Inherited from +Meta value of the mark -```ts -MarkStruct.label -``` +##### Returns -*** +`string` \| `undefined` -### meta? +#### Set Signature ```ts -optional meta: string; +set meta(value): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:35](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L35) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:82](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L82) -Meta value of the mark +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `value` | `string` \| `undefined` | + +##### Returns + +`void` *** ### parent +#### Get Signature + ```ts -readonly parent: MarkToken | undefined; +get parent(): MarkToken | undefined; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:49](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L49) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:110](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L110) Parent mark token (undefined for root-level marks). -Computed lazily on access. +Computed lazily on access - O(n) traversal. + +##### Returns + +[`MarkToken`](/api/interfaces/marktoken/) \| `undefined` *** -### readOnly? +### tokens + +#### Get Signature ```ts -optional readOnly: boolean; +get tokens(): Token[]; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:31](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L31) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:117](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L117) + +Array of child tokens (read-only) + +##### Returns -Passed the readOnly prop value +[`Token`](/api/type-aliases/token/)[] *** -### ref +### value + +#### Get Signature ```ts -ref: RefObject; +get value(): string | undefined; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:17](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L17) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:66](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L66) -MarkStruct ref. Used for focusing and key handling operations. +Value of the mark (hidden data) -*** +##### Returns -### remove() +`string` \| `undefined` + +#### Set Signature ```ts -remove: () => void; +set value(value): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:27](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L27) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:70](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L70) -Remove itself. +##### Parameters -#### Returns +| Parameter | Type | +| ------ | ------ | +| `value` | `string` \| `undefined` | + +##### Returns `void` -*** +## Methods -### tokens +### change() ```ts -readonly tokens: Token[]; +change(props): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:53](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L53) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:124](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L124) -Array of child tokens (read-only) +Change mark content, value, and/or meta at once. + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `props` | \{ `content`: `string`; `meta?`: `string`; `value?`: `string`; \} | +| `props.content` | `string` | +| `props.meta?` | `string` | +| `props.value?` | `string` | + +#### Returns + +`void` *** -### value? +### remove() ```ts -optional value: string; +remove(): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:10](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L10) +Defined in: [packages/markput/src/utils/hooks/useMark.tsx:136](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L136) -#### Inherited from +Remove this mark. -```ts -MarkStruct.value -``` +#### Returns + +`void` From cc824ec7edee7a7f260744d3f3f33122959e9b55 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 14:41:10 +0300 Subject: [PATCH 04/13] Rename utils to lib --- packages/markput/index.ts | 12 ++++---- packages/markput/src/components/Container.tsx | 6 ++-- .../markput/src/components/EditableSpan.tsx | 6 ++-- packages/markput/src/components/Piece.tsx | 6 ++-- .../markput/src/components/StoreProvider.tsx | 4 +-- .../components/Suggestions/Suggestions.tsx | 4 +-- packages/markput/src/components/TextSpan.tsx | 6 ++-- packages/markput/src/components/Token.tsx | 2 +- packages/markput/src/components/Whisper.tsx | 4 +-- .../features/events/useCloseOverlayByEsc.tsx | 2 +- .../events/useCloseOverlayByOutsideClick.tsx | 2 +- .../src/features/events/useKeyDown.tsx | 6 ++-- .../features/events/useSystemListeners.tsx | 4 +-- .../features/focus/useFocusOnEmptyInput.tsx | 4 +-- .../src/features/focus/useFocusRecovery.tsx | 2 +- .../src/features/focus/useFocusedNode.tsx | 4 +-- .../src/features/focus/useTextSelection.tsx | 2 +- .../src/features/overlay/useCheckTrigger.tsx | 4 +-- .../src/features/overlay/useTrigger.tsx | 4 +-- .../src/features/parsing/useValueParser.tsx | 4 +-- .../src/features/useMarkedInputHandler.tsx | 2 +- .../{utils => lib}/functions/createContext.ts | 0 .../functions/createMarkedInput.ts | 0 .../functions/dataAttributes.ts | 0 .../{utils => lib}/functions/resolveSlot.ts | 0 .../src/{utils => lib}/hooks/useDownOf.tsx | 0 .../src/{utils => lib}/hooks/useListener.tsx | 0 .../src/{utils => lib}/hooks/useMark.tsx | 0 .../src/{utils => lib}/hooks/useOverlay.tsx | 0 .../src/{utils => lib}/hooks/useSlot.ts | 0 .../src/{utils => lib}/hooks/useStore.ts | 0 .../{utils => lib}/providers/StoreContext.ts | 0 .../{utils => lib}/providers/TokenProvider.ts | 0 .../docs/api/functions/createMarkedInput.md | 2 +- .../content/docs/api/functions/useListener.md | 4 +-- .../src/content/docs/api/functions/useMark.md | 2 +- .../content/docs/api/functions/useOverlay.md | 2 +- .../docs/api/interfaces/MarkHandler.md | 30 +++++++++---------- .../docs/api/interfaces/OverlayHandler.md | 12 ++++---- 39 files changed, 71 insertions(+), 71 deletions(-) rename packages/markput/src/{utils => lib}/functions/createContext.ts (100%) rename packages/markput/src/{utils => lib}/functions/createMarkedInput.ts (100%) rename packages/markput/src/{utils => lib}/functions/dataAttributes.ts (100%) rename packages/markput/src/{utils => lib}/functions/resolveSlot.ts (100%) rename packages/markput/src/{utils => lib}/hooks/useDownOf.tsx (100%) rename packages/markput/src/{utils => lib}/hooks/useListener.tsx (100%) rename packages/markput/src/{utils => lib}/hooks/useMark.tsx (100%) rename packages/markput/src/{utils => lib}/hooks/useOverlay.tsx (100%) rename packages/markput/src/{utils => lib}/hooks/useSlot.ts (100%) rename packages/markput/src/{utils => lib}/hooks/useStore.ts (100%) rename packages/markput/src/{utils => lib}/providers/StoreContext.ts (100%) rename packages/markput/src/{utils => lib}/providers/TokenProvider.ts (100%) diff --git a/packages/markput/index.ts b/packages/markput/index.ts index a7724b33..e640710b 100644 --- a/packages/markput/index.ts +++ b/packages/markput/index.ts @@ -1,12 +1,12 @@ export {MarkedInput} from './src/components/MarkedInput' -export {createMarkedInput} from './src/utils/functions/createMarkedInput' -export {useMark} from './src/utils/hooks/useMark' -export {useOverlay} from './src/utils/hooks/useOverlay' -export {useListener} from './src/utils/hooks/useListener' +export {createMarkedInput} from './src/lib/functions/createMarkedInput' +export {useMark} from './src/lib/hooks/useMark' +export {useOverlay} from './src/lib/hooks/useOverlay' +export {useListener} from './src/lib/hooks/useListener' export type {MarkedInputProps, MarkedInputComponent} from './src/components/MarkedInput' -export type {MarkHandler} from './src/utils/hooks/useMark' -export type {OverlayHandler} from './src/utils/hooks/useOverlay' +export type {MarkHandler} from './src/lib/hooks/useMark' +export type {OverlayHandler} from './src/lib/hooks/useOverlay' export type {MarkedInputHandler, Option, ConfiguredMarkedInput, MarkProps, OverlayProps} from './src/types' // Re-export ParserV2 functions and types diff --git a/packages/markput/src/components/Container.tsx b/packages/markput/src/components/Container.tsx index 647ee99a..b5b0d6d6 100644 --- a/packages/markput/src/components/Container.tsx +++ b/packages/markput/src/components/Container.tsx @@ -1,7 +1,7 @@ import {memo} from 'react' -import {resolveSlot, resolveSlotProps} from '../utils/functions/resolveSlot' -import {useListener} from '../utils/hooks/useListener' -import {useStore} from '../utils/hooks/useStore' +import {resolveSlot, resolveSlotProps} from '../lib/functions/resolveSlot' +import {useListener} from '../lib/hooks/useListener' +import {useStore} from '../lib/hooks/useStore' import {Token} from './Token' import {SystemEvent} from '@markput/core' diff --git a/packages/markput/src/components/EditableSpan.tsx b/packages/markput/src/components/EditableSpan.tsx index ff4cf5a8..608ccd44 100644 --- a/packages/markput/src/components/EditableSpan.tsx +++ b/packages/markput/src/components/EditableSpan.tsx @@ -1,7 +1,7 @@ import type {ClipboardEvent} from 'react' -import {resolveSlot, resolveSlotProps} from '../utils/functions/resolveSlot' -import {useMark} from '../utils/hooks/useMark' -import {useStore} from '../utils/hooks/useStore' +import {resolveSlot, resolveSlotProps} from '../lib/functions/resolveSlot' +import {useMark} from '../lib/hooks/useMark' +import {useStore} from '../lib/hooks/useStore' //Editable block - edit text here export const EditableSpan = () => { diff --git a/packages/markput/src/components/Piece.tsx b/packages/markput/src/components/Piece.tsx index 14b63d28..b0ad39d7 100644 --- a/packages/markput/src/components/Piece.tsx +++ b/packages/markput/src/components/Piece.tsx @@ -1,7 +1,7 @@ import type {MarkToken} from '@markput/core' -import {useStore} from '../utils/hooks/useStore' -import {useSlot} from '../utils/hooks/useSlot' -import {useToken} from '../utils/providers/TokenProvider' +import {useStore} from '../lib/hooks/useStore' +import {useSlot} from '../lib/hooks/useSlot' +import {useToken} from '../lib/providers/TokenProvider' import type {MarkProps} from '../types' // eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Piece → Token → MarkTokenComponent → Piece import {Token} from './Token' diff --git a/packages/markput/src/components/StoreProvider.tsx b/packages/markput/src/components/StoreProvider.tsx index 8938eb7f..c693f296 100644 --- a/packages/markput/src/components/StoreProvider.tsx +++ b/packages/markput/src/components/StoreProvider.tsx @@ -2,8 +2,8 @@ import type {ReactNode} from 'react' import {useEffect, useState} from 'react' import {Store, DEFAULT_CLASS_NAME} from '@markput/core' import type {MarkedInputProps} from './MarkedInput' -import {StoreContext} from '../utils/providers/StoreContext' -import {mergeClassNames, mergeStyles} from '../utils/functions/resolveSlot' +import {StoreContext} from '../lib/providers/StoreContext' +import {mergeClassNames, mergeStyles} from '../lib/functions/resolveSlot' import {DEFAULT_OPTIONS} from '../constants' interface StoreProviderProps { diff --git a/packages/markput/src/components/Suggestions/Suggestions.tsx b/packages/markput/src/components/Suggestions/Suggestions.tsx index 0f11f4ff..7de8919d 100644 --- a/packages/markput/src/components/Suggestions/Suggestions.tsx +++ b/packages/markput/src/components/Suggestions/Suggestions.tsx @@ -1,7 +1,7 @@ import type {RefObject} from 'react' import {useMemo, useState} from 'react' -import {useDownOf} from '../../utils/hooks/useDownOf' -import {useOverlay} from '../../utils/hooks/useOverlay' +import {useDownOf} from '../../lib/hooks/useDownOf' +import {useOverlay} from '../../lib/hooks/useOverlay' import {KEYBOARD} from '@markput/core' export const Suggestions = () => { diff --git a/packages/markput/src/components/TextSpan.tsx b/packages/markput/src/components/TextSpan.tsx index a0cd0d78..a913a7b9 100644 --- a/packages/markput/src/components/TextSpan.tsx +++ b/packages/markput/src/components/TextSpan.tsx @@ -1,8 +1,8 @@ import type {ClipboardEvent} from 'react' import {useRef} from 'react' -import {resolveSlot, resolveSlotProps} from '../utils/functions/resolveSlot' -import {useStore} from '../utils/hooks/useStore' -import {useToken} from '../utils/providers/TokenProvider' +import {resolveSlot, resolveSlotProps} from '../lib/functions/resolveSlot' +import {useStore} from '../lib/hooks/useStore' +import {useToken} from '../lib/providers/TokenProvider' /** * TextSpan - renders text tokens (non-annotated text) diff --git a/packages/markput/src/components/Token.tsx b/packages/markput/src/components/Token.tsx index b9d33861..264fa054 100644 --- a/packages/markput/src/components/Token.tsx +++ b/packages/markput/src/components/Token.tsx @@ -1,6 +1,6 @@ import {memo} from 'react' import type {MarkToken, TextToken, Token as TokenType} from '@markput/core' -import {TokenProvider} from '../utils/providers/TokenProvider' +import {TokenProvider} from '../lib/providers/TokenProvider' // eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token → Piece → Token import {Piece} from './Piece' import {TextSpan} from './TextSpan' diff --git a/packages/markput/src/components/Whisper.tsx b/packages/markput/src/components/Whisper.tsx index 1665e505..c7a79d2d 100644 --- a/packages/markput/src/components/Whisper.tsx +++ b/packages/markput/src/components/Whisper.tsx @@ -1,6 +1,6 @@ import {memo, useEffect} from 'react' -import {useStore} from '../utils/hooks/useStore' -import {useSlot} from '../utils/hooks/useSlot' +import {useStore} from '../lib/hooks/useStore' +import {useSlot} from '../lib/hooks/useSlot' import {Suggestions} from './Suggestions' /** diff --git a/packages/markput/src/features/events/useCloseOverlayByEsc.tsx b/packages/markput/src/features/events/useCloseOverlayByEsc.tsx index 0d8ed8d6..bcba7393 100644 --- a/packages/markput/src/features/events/useCloseOverlayByEsc.tsx +++ b/packages/markput/src/features/events/useCloseOverlayByEsc.tsx @@ -1,5 +1,5 @@ import {useEffect} from 'react' -import {useStore} from '../../utils/hooks/useStore' +import {useStore} from '../../lib/hooks/useStore' import {KEYBOARD, SystemEvent} from '@markput/core' export function useCloseOverlayByEsc() { diff --git a/packages/markput/src/features/events/useCloseOverlayByOutsideClick.tsx b/packages/markput/src/features/events/useCloseOverlayByOutsideClick.tsx index f0b65547..96192269 100644 --- a/packages/markput/src/features/events/useCloseOverlayByOutsideClick.tsx +++ b/packages/markput/src/features/events/useCloseOverlayByOutsideClick.tsx @@ -1,5 +1,5 @@ import {useEffect} from 'react' -import {useStore} from '../../utils/hooks/useStore' +import {useStore} from '../../lib/hooks/useStore' import {SystemEvent} from '@markput/core' export function useCloseOverlayByOutsideClick() { diff --git a/packages/markput/src/features/events/useKeyDown.tsx b/packages/markput/src/features/events/useKeyDown.tsx index 1a7a547d..d0b453af 100644 --- a/packages/markput/src/features/events/useKeyDown.tsx +++ b/packages/markput/src/features/events/useKeyDown.tsx @@ -1,7 +1,7 @@ import {deleteMark, KEYBOARD} from '@markput/core' -import {useDownOf} from '../../utils/hooks/useDownOf' -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useDownOf} from '../../lib/hooks/useDownOf' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' //TODO Focus on mark and attribute for this //TODO different rules for editable diff --git a/packages/markput/src/features/events/useSystemListeners.tsx b/packages/markput/src/features/events/useSystemListeners.tsx index c1cbc23d..db38d684 100644 --- a/packages/markput/src/features/events/useSystemListeners.tsx +++ b/packages/markput/src/features/events/useSystemListeners.tsx @@ -1,6 +1,6 @@ import {SystemEvent, annotate, createNewSpan, toString} from '@markput/core' -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' //TODO upgrade to full members of react events to external export function useSystemListeners() { diff --git a/packages/markput/src/features/focus/useFocusOnEmptyInput.tsx b/packages/markput/src/features/focus/useFocusOnEmptyInput.tsx index d25577a6..05a81671 100644 --- a/packages/markput/src/features/focus/useFocusOnEmptyInput.tsx +++ b/packages/markput/src/features/focus/useFocusOnEmptyInput.tsx @@ -1,5 +1,5 @@ -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' export const useFocusOnEmptyInput = () => { const store = useStore() diff --git a/packages/markput/src/features/focus/useFocusRecovery.tsx b/packages/markput/src/features/focus/useFocusRecovery.tsx index b49d13b2..b2c7cf14 100644 --- a/packages/markput/src/features/focus/useFocusRecovery.tsx +++ b/packages/markput/src/features/focus/useFocusRecovery.tsx @@ -1,5 +1,5 @@ import {useEffect} from 'react' -import {useStore} from '../../utils/hooks/useStore' +import {useStore} from '../../lib/hooks/useStore' export const useFocusRecovery = () => { const store = useStore() diff --git a/packages/markput/src/features/focus/useFocusedNode.tsx b/packages/markput/src/features/focus/useFocusedNode.tsx index e1070635..8aa10240 100644 --- a/packages/markput/src/features/focus/useFocusedNode.tsx +++ b/packages/markput/src/features/focus/useFocusedNode.tsx @@ -1,5 +1,5 @@ -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' export const useFocusedNode = () => { const store = useStore() diff --git a/packages/markput/src/features/focus/useTextSelection.tsx b/packages/markput/src/features/focus/useTextSelection.tsx index 66e880e9..f8380314 100644 --- a/packages/markput/src/features/focus/useTextSelection.tsx +++ b/packages/markput/src/features/focus/useTextSelection.tsx @@ -1,5 +1,5 @@ import {useEffect, useRef} from 'react' -import {useStore} from '../../utils/hooks/useStore' +import {useStore} from '../../lib/hooks/useStore' export function useTextSelection() { const store = useStore() diff --git a/packages/markput/src/features/overlay/useCheckTrigger.tsx b/packages/markput/src/features/overlay/useCheckTrigger.tsx index 6550c4d9..2748b4ef 100644 --- a/packages/markput/src/features/overlay/useCheckTrigger.tsx +++ b/packages/markput/src/features/overlay/useCheckTrigger.tsx @@ -1,6 +1,6 @@ import {useCallback} from 'react' -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' import type {OverlayTrigger} from '@markput/core' import {SystemEvent} from '@markput/core' diff --git a/packages/markput/src/features/overlay/useTrigger.tsx b/packages/markput/src/features/overlay/useTrigger.tsx index c71cd2c1..38f6a6fb 100644 --- a/packages/markput/src/features/overlay/useTrigger.tsx +++ b/packages/markput/src/features/overlay/useTrigger.tsx @@ -1,7 +1,7 @@ import {SystemEvent, TriggerFinder} from '@markput/core' import type {Option} from '../../types' -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' export const useTrigger = () => { const store = useStore() diff --git a/packages/markput/src/features/parsing/useValueParser.tsx b/packages/markput/src/features/parsing/useValueParser.tsx index e54c88ad..8310fb01 100644 --- a/packages/markput/src/features/parsing/useValueParser.tsx +++ b/packages/markput/src/features/parsing/useValueParser.tsx @@ -1,6 +1,6 @@ import {useEffect, useRef} from 'react' -import {useListener} from '../../utils/hooks/useListener' -import {useStore} from '../../utils/hooks/useStore' +import {useListener} from '../../lib/hooks/useListener' +import {useStore} from '../../lib/hooks/useStore' import type {Store} from '@markput/core' import {Parser, SystemEvent, findGap, getClosestIndexes} from '@markput/core' diff --git a/packages/markput/src/features/useMarkedInputHandler.tsx b/packages/markput/src/features/useMarkedInputHandler.tsx index f5c9ee7d..d9c45114 100644 --- a/packages/markput/src/features/useMarkedInputHandler.tsx +++ b/packages/markput/src/features/useMarkedInputHandler.tsx @@ -2,7 +2,7 @@ import type {ForwardedRef} from 'react' import {useImperativeHandle} from 'react' import type {MarkedInputHandler} from '../types' import type {Store} from '@markput/core' -import {useStore} from '../utils/hooks/useStore' +import {useStore} from '../lib/hooks/useStore' const initHandler = (store: Store): MarkedInputHandler => ({ get container() { diff --git a/packages/markput/src/utils/functions/createContext.ts b/packages/markput/src/lib/functions/createContext.ts similarity index 100% rename from packages/markput/src/utils/functions/createContext.ts rename to packages/markput/src/lib/functions/createContext.ts diff --git a/packages/markput/src/utils/functions/createMarkedInput.ts b/packages/markput/src/lib/functions/createMarkedInput.ts similarity index 100% rename from packages/markput/src/utils/functions/createMarkedInput.ts rename to packages/markput/src/lib/functions/createMarkedInput.ts diff --git a/packages/markput/src/utils/functions/dataAttributes.ts b/packages/markput/src/lib/functions/dataAttributes.ts similarity index 100% rename from packages/markput/src/utils/functions/dataAttributes.ts rename to packages/markput/src/lib/functions/dataAttributes.ts diff --git a/packages/markput/src/utils/functions/resolveSlot.ts b/packages/markput/src/lib/functions/resolveSlot.ts similarity index 100% rename from packages/markput/src/utils/functions/resolveSlot.ts rename to packages/markput/src/lib/functions/resolveSlot.ts diff --git a/packages/markput/src/utils/hooks/useDownOf.tsx b/packages/markput/src/lib/hooks/useDownOf.tsx similarity index 100% rename from packages/markput/src/utils/hooks/useDownOf.tsx rename to packages/markput/src/lib/hooks/useDownOf.tsx diff --git a/packages/markput/src/utils/hooks/useListener.tsx b/packages/markput/src/lib/hooks/useListener.tsx similarity index 100% rename from packages/markput/src/utils/hooks/useListener.tsx rename to packages/markput/src/lib/hooks/useListener.tsx diff --git a/packages/markput/src/utils/hooks/useMark.tsx b/packages/markput/src/lib/hooks/useMark.tsx similarity index 100% rename from packages/markput/src/utils/hooks/useMark.tsx rename to packages/markput/src/lib/hooks/useMark.tsx diff --git a/packages/markput/src/utils/hooks/useOverlay.tsx b/packages/markput/src/lib/hooks/useOverlay.tsx similarity index 100% rename from packages/markput/src/utils/hooks/useOverlay.tsx rename to packages/markput/src/lib/hooks/useOverlay.tsx diff --git a/packages/markput/src/utils/hooks/useSlot.ts b/packages/markput/src/lib/hooks/useSlot.ts similarity index 100% rename from packages/markput/src/utils/hooks/useSlot.ts rename to packages/markput/src/lib/hooks/useSlot.ts diff --git a/packages/markput/src/utils/hooks/useStore.ts b/packages/markput/src/lib/hooks/useStore.ts similarity index 100% rename from packages/markput/src/utils/hooks/useStore.ts rename to packages/markput/src/lib/hooks/useStore.ts diff --git a/packages/markput/src/utils/providers/StoreContext.ts b/packages/markput/src/lib/providers/StoreContext.ts similarity index 100% rename from packages/markput/src/utils/providers/StoreContext.ts rename to packages/markput/src/lib/providers/StoreContext.ts diff --git a/packages/markput/src/utils/providers/TokenProvider.ts b/packages/markput/src/lib/providers/TokenProvider.ts similarity index 100% rename from packages/markput/src/utils/providers/TokenProvider.ts rename to packages/markput/src/lib/providers/TokenProvider.ts diff --git a/packages/website/src/content/docs/api/functions/createMarkedInput.md b/packages/website/src/content/docs/api/functions/createMarkedInput.md index 1e95bfad..34b48f3a 100644 --- a/packages/website/src/content/docs/api/functions/createMarkedInput.md +++ b/packages/website/src/content/docs/api/functions/createMarkedInput.md @@ -9,7 +9,7 @@ title: "createMarkedInput" function createMarkedInput(configs): ConfiguredMarkedInput; ``` -Defined in: [packages/markput/src/utils/functions/createMarkedInput.ts:13](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/functions/createMarkedInput.ts#L13) +Defined in: [packages/markput/src/lib/functions/createMarkedInput.ts:13](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/functions/createMarkedInput.ts#L13) Create the configured MarkedInput component. diff --git a/packages/website/src/content/docs/api/functions/useListener.md b/packages/website/src/content/docs/api/functions/useListener.md index 18e6229b..a8445d7f 100644 --- a/packages/website/src/content/docs/api/functions/useListener.md +++ b/packages/website/src/content/docs/api/functions/useListener.md @@ -14,7 +14,7 @@ function useListener( deps?): void; ``` -Defined in: [packages/markput/src/utils/hooks/useListener.tsx:7](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useListener.tsx#L7) +Defined in: [packages/markput/src/lib/hooks/useListener.tsx:7](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useListener.tsx#L7) ### Type Parameters @@ -43,7 +43,7 @@ function useListener( deps?): void; ``` -Defined in: [packages/markput/src/utils/hooks/useListener.tsx:8](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useListener.tsx#L8) +Defined in: [packages/markput/src/lib/hooks/useListener.tsx:8](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useListener.tsx#L8) ### Type Parameters diff --git a/packages/website/src/content/docs/api/functions/useMark.md b/packages/website/src/content/docs/api/functions/useMark.md index 91936067..f6180d5d 100644 --- a/packages/website/src/content/docs/api/functions/useMark.md +++ b/packages/website/src/content/docs/api/functions/useMark.md @@ -9,7 +9,7 @@ title: "useMark" function useMark(options): MarkHandler; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:16](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L16) +Defined in: [packages/markput/src/lib/hooks/useMark.tsx:18](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useMark.tsx#L18) ## Type Parameters diff --git a/packages/website/src/content/docs/api/functions/useOverlay.md b/packages/website/src/content/docs/api/functions/useOverlay.md index 6aa994d9..dbcb1726 100644 --- a/packages/website/src/content/docs/api/functions/useOverlay.md +++ b/packages/website/src/content/docs/api/functions/useOverlay.md @@ -9,7 +9,7 @@ title: "useOverlay" function useOverlay(): OverlayHandler; ``` -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:31](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L31) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:31](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L31) ## Returns diff --git a/packages/website/src/content/docs/api/interfaces/MarkHandler.md b/packages/website/src/content/docs/api/interfaces/MarkHandler.md index ab1db5f4..a824bef3 100644 --- a/packages/website/src/content/docs/api/interfaces/MarkHandler.md +++ b/packages/website/src/content/docs/api/interfaces/MarkHandler.md @@ -5,7 +5,7 @@ prev: false title: "MarkHandler" --- -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:38](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L38) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:5 ## Type Parameters @@ -21,7 +21,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.tsx:38](https://github.com optional readOnly: boolean; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:43](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L43) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:10 *** @@ -31,7 +31,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.tsx:43](https://github.com readonly ref: RefObject; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:39](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L39) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:6 ## Accessors @@ -43,7 +43,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.tsx:39](https://github.com get content(): string; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:54](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L54) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:21 Content/label of the mark (displayed text) @@ -57,7 +57,7 @@ Content/label of the mark (displayed text) set content(value): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:58](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L58) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:25 ##### Parameters @@ -79,7 +79,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.tsx:58](https://github.com get depth(): number; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:95](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L95) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:62 Nesting depth of this mark (0 for root-level marks). Computed lazily on access - O(n) traversal. @@ -98,7 +98,7 @@ Computed lazily on access - O(n) traversal. get hasChildren(): boolean; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:102](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L102) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:69 Whether this mark has nested children @@ -116,7 +116,7 @@ Whether this mark has nested children get meta(): string | undefined; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:78](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L78) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:45 Meta value of the mark @@ -130,7 +130,7 @@ Meta value of the mark set meta(value): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:82](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L82) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:49 ##### Parameters @@ -152,7 +152,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.tsx:82](https://github.com get parent(): MarkToken | undefined; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:110](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L110) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:77 Parent mark token (undefined for root-level marks). Computed lazily on access - O(n) traversal. @@ -171,7 +171,7 @@ Computed lazily on access - O(n) traversal. get tokens(): Token[]; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:117](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L117) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:84 Array of child tokens (read-only) @@ -189,7 +189,7 @@ Array of child tokens (read-only) get value(): string | undefined; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:66](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L66) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:33 Value of the mark (hidden data) @@ -203,7 +203,7 @@ Value of the mark (hidden data) set value(value): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:70](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L70) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:37 ##### Parameters @@ -223,7 +223,7 @@ Defined in: [packages/markput/src/utils/hooks/useMark.tsx:70](https://github.com change(props): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:124](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L124) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:91 Change mark content, value, and/or meta at once. @@ -248,7 +248,7 @@ Change mark content, value, and/or meta at once. remove(): void; ``` -Defined in: [packages/markput/src/utils/hooks/useMark.tsx:136](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.tsx#L136) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:103 Remove this mark. diff --git a/packages/website/src/content/docs/api/interfaces/OverlayHandler.md b/packages/website/src/content/docs/api/interfaces/OverlayHandler.md index b912ffd0..580311fa 100644 --- a/packages/website/src/content/docs/api/interfaces/OverlayHandler.md +++ b/packages/website/src/content/docs/api/interfaces/OverlayHandler.md @@ -5,7 +5,7 @@ prev: false title: "OverlayHandler" --- -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:8](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L8) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:8](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L8) ## Properties @@ -15,7 +15,7 @@ Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:8](https://github.c close: () => void; ``` -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:19](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L19) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:19](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L19) Used for close overlay. @@ -31,7 +31,7 @@ Used for close overlay. match: OverlayMatch>; ``` -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:27](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L27) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:27](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L27) Overlay match details @@ -43,7 +43,7 @@ Overlay match details ref: RefObject; ``` -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:28](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L28) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:28](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L28) *** @@ -53,7 +53,7 @@ Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:28](https://github. select: (value) => void; ``` -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:23](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L23) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:23](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L23) Used for insert an annotation instead a triggered value. @@ -77,7 +77,7 @@ Used for insert an annotation instead a triggered value. style: object; ``` -Defined in: [packages/markput/src/utils/hooks/useOverlay.tsx:12](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useOverlay.tsx#L12) +Defined in: [packages/markput/src/lib/hooks/useOverlay.tsx:12](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/lib/hooks/useOverlay.tsx#L12) Style with caret absolute position. Used for placing an overlay. From 667f99d1dbe6bab4a0f4ded0e6119faaf1e39046 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 14:52:12 +0300 Subject: [PATCH 05/13] Add MarkHandler class to manage mark properties and functionality - Introduced the MarkHandler class to encapsulate mark management, including content, value, and meta properties. - Implemented methods for changing mark properties and removing marks, enhancing interaction with the store. - Added utility functions for calculating nesting depth and finding parent marks, improving mark structure handling. - Refactored useMark hook to utilize the new MarkHandler class, streamlining mark-related logic and improving clarity. --- .../markput/src/lib/classes/MarkHandler.ts | 158 +++++++++++++++++ packages/markput/src/lib/hooks/useMark.tsx | 159 +----------------- 2 files changed, 160 insertions(+), 157 deletions(-) create mode 100644 packages/markput/src/lib/classes/MarkHandler.ts diff --git a/packages/markput/src/lib/classes/MarkHandler.ts b/packages/markput/src/lib/classes/MarkHandler.ts new file mode 100644 index 00000000..3cbe8f30 --- /dev/null +++ b/packages/markput/src/lib/classes/MarkHandler.ts @@ -0,0 +1,158 @@ +import type {RefObject} from 'react' +import type {MarkToken, Store, Token} from '@markput/core' +import {SystemEvent} from '@markput/core' + +export class MarkHandler { + readonly ref: RefObject + readonly #store: Store + readonly #token: MarkToken + + readOnly?: boolean + + constructor(param: {ref: RefObject; store: Store; token: MarkToken}) { + this.ref = param.ref + this.#store = param.store + this.#token = param.token + } + + /** + * Content/label of the mark (displayed text) + */ + get content() { + return this.#token.content + } + + set content(value: string) { + this.#token.content = value + this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + } + + /** + * Value of the mark (hidden data) + */ + get value() { + return this.#token.value + } + + set value(value: string | undefined) { + this.#token.value = value ?? '' + this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + } + + /** + * Meta value of the mark + */ + get meta() { + return this.#token.meta + } + + set meta(value: string | undefined) { + if (value !== undefined) { + this.#token.meta = value + } else { + delete this.#token.meta + } + this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + } + + /** + * Nesting depth of this mark (0 for root-level marks). + * Computed lazily on access - O(n) traversal. + */ + get depth(): number { + return calculateDepth(this.#token, this.#store.tokens) + } + + /** + * Whether this mark has nested children + */ + get hasChildren(): boolean { + return this.#token.children.length > 0 + } + + /** + * Parent mark token (undefined for root-level marks). + * Computed lazily on access - O(n) traversal. + */ + get parent(): MarkToken | undefined { + return findParent(this.#token, this.#store.tokens) + } + + /** + * Array of child tokens (read-only) + */ + get tokens(): Token[] { + return this.#token.children + } + + /** + * Change mark content, value, and/or meta at once. + */ + change = (props: {content: string; value?: string; meta?: string}) => { + this.#token.content = props.content + this.#token.value = props.value ?? '' + if (props.meta !== undefined) { + this.#token.meta = props.meta + } + this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + } + + /** + * Remove this mark. + */ + remove = () => this.#store.bus.send(SystemEvent.Delete, {token: this.#token}) +} + +/** + * Calculate the nesting depth of a token in the tree. + * O(n) traversal - only called when depth is accessed. + */ +function calculateDepth(token: MarkToken, tokens: Token[]): number { + let depth = 0 + + function findDepthRecursive(currentTokens: Token[], currentDepth: number): boolean { + for (const t of currentTokens) { + if (t === token) { + depth = currentDepth + return true + } + + if (t.type === 'mark' && t.children.length > 0) { + if (findDepthRecursive(t.children, currentDepth + 1)) { + return true + } + } + } + return false + } + + findDepthRecursive(tokens, 0) + return depth +} + +/** + * Find the parent MarkToken of a given token. + * O(n) traversal - only called when parent is accessed. + */ +function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { + let parent: MarkToken | undefined + + function findParentRecursive(currentTokens: Token[], currentParent?: MarkToken): boolean { + for (const t of currentTokens) { + if (t === token) { + parent = currentParent + return true + } + + if (t.type === 'mark' && t.children.length > 0) { + if (findParentRecursive(t.children, t)) { + return true + } + } + } + return false + } + + findParentRecursive(tokens) + return parent +} diff --git a/packages/markput/src/lib/hooks/useMark.tsx b/packages/markput/src/lib/hooks/useMark.tsx index c89640b6..a3f96280 100644 --- a/packages/markput/src/lib/hooks/useMark.tsx +++ b/packages/markput/src/lib/hooks/useMark.tsx @@ -1,9 +1,9 @@ import type {RefObject} from 'react' import {useEffect, useRef, useState} from 'react' -import type {MarkToken, Store, Token} from '@markput/core' -import {SystemEvent} from '@markput/core' +import type {MarkToken} from '@markput/core' import {useToken} from '../providers/TokenProvider' import {useStore} from './useStore' +import {MarkHandler} from '../classes/MarkHandler' export interface MarkOptions { /** @@ -35,161 +35,6 @@ export const useMark = (options: MarkOption return mark } -export class MarkHandler { - readonly ref: RefObject - readonly #store: Store - readonly #token: MarkToken - - readOnly?: boolean - - constructor(param: {ref: RefObject; store: Store; token: MarkToken}) { - this.ref = param.ref - this.#store = param.store - this.#token = param.token - } - - /** - * Content/label of the mark (displayed text) - */ - get content() { - return this.#token.content - } - - set content(value: string) { - this.#token.content = value - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) - } - - /** - * Value of the mark (hidden data) - */ - get value() { - return this.#token.value - } - - set value(value: string | undefined) { - this.#token.value = value ?? '' - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) - } - - /** - * Meta value of the mark - */ - get meta() { - return this.#token.meta - } - - set meta(value: string | undefined) { - if (value !== undefined) { - this.#token.meta = value - } else { - delete this.#token.meta - } - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) - } - - /** - * Nesting depth of this mark (0 for root-level marks). - * Computed lazily on access - O(n) traversal. - */ - get depth(): number { - return calculateDepth(this.#token, this.#store.tokens) - } - - /** - * Whether this mark has nested children - */ - get hasChildren(): boolean { - return this.#token.children.length > 0 - } - - /** - * Parent mark token (undefined for root-level marks). - * Computed lazily on access - O(n) traversal. - */ - get parent(): MarkToken | undefined { - return findParent(this.#token, this.#store.tokens) - } - - /** - * Array of child tokens (read-only) - */ - get tokens(): Token[] { - return this.#token.children - } - - /** - * Change mark content, value, and/or meta at once. - */ - change = (props: {content: string; value?: string; meta?: string}) => { - this.#token.content = props.content - this.#token.value = props.value ?? '' - if (props.meta !== undefined) { - this.#token.meta = props.meta - } - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) - } - - /** - * Remove this mark. - */ - remove = () => this.#store.bus.send(SystemEvent.Delete, {token: this.#token}) -} - -/** - * Calculate the nesting depth of a token in the tree. - * O(n) traversal - only called when depth is accessed. - */ -function calculateDepth(token: MarkToken, tokens: Token[]): number { - let depth = 0 - - function findDepthRecursive(currentTokens: Token[], currentDepth: number): boolean { - for (const t of currentTokens) { - if (t === token) { - depth = currentDepth - return true - } - - if (t.type === 'mark' && t.children.length > 0) { - if (findDepthRecursive(t.children, currentDepth + 1)) { - return true - } - } - } - return false - } - - findDepthRecursive(tokens, 0) - return depth -} - -/** - * Find the parent MarkToken of a given token. - * O(n) traversal - only called when parent is accessed. - */ -function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { - let parent: MarkToken | undefined - - function findParentRecursive(currentTokens: Token[], currentParent?: MarkToken): boolean { - for (const t of currentTokens) { - if (t === token) { - parent = currentParent - return true - } - - if (t.type === 'mark' && t.children.length > 0) { - if (findParentRecursive(t.children, t)) { - return true - } - } - } - return false - } - - findParentRecursive(tokens) - return parent -} - function useUncontrolledInit(ref: RefObject, options: MarkOptions, token: MarkToken) { useEffect(() => { if (ref.current && !options.controlled) ref.current.textContent = token.content From cfefbd485e2a3cd859c0a22e51b78e92c4b61241 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:07:22 +0300 Subject: [PATCH 06/13] Refactor MarkHandler class to improve property management and documentation - Updated the MarkHandler class to streamline property management by consolidating change notifications into a private method. - Enhanced documentation for properties and methods, improving clarity and organization. - Removed redundant comments and improved the structure of the code for better readability. - Adjusted the export path for MarkHandler in the index file to reflect the new class location. --- packages/markput/index.ts | 2 +- .../markput/src/lib/classes/MarkHandler.ts | 87 +++++++------------ 2 files changed, 34 insertions(+), 55 deletions(-) diff --git a/packages/markput/index.ts b/packages/markput/index.ts index e640710b..ebf9c31a 100644 --- a/packages/markput/index.ts +++ b/packages/markput/index.ts @@ -5,7 +5,7 @@ export {useOverlay} from './src/lib/hooks/useOverlay' export {useListener} from './src/lib/hooks/useListener' export type {MarkedInputProps, MarkedInputComponent} from './src/components/MarkedInput' -export type {MarkHandler} from './src/lib/hooks/useMark' +export type {MarkHandler} from './src/lib/classes/MarkHandler' export type {OverlayHandler} from './src/lib/hooks/useOverlay' export type {MarkedInputHandler, Option, ConfiguredMarkedInput, MarkProps, OverlayProps} from './src/types' diff --git a/packages/markput/src/lib/classes/MarkHandler.ts b/packages/markput/src/lib/classes/MarkHandler.ts index 3cbe8f30..ec265298 100644 --- a/packages/markput/src/lib/classes/MarkHandler.ts +++ b/packages/markput/src/lib/classes/MarkHandler.ts @@ -6,7 +6,6 @@ export class MarkHandler { readonly ref: RefObject readonly #store: Store readonly #token: MarkToken - readOnly?: boolean constructor(param: {ref: RefObject; store: Store; token: MarkToken}) { @@ -15,100 +14,88 @@ export class MarkHandler { this.#token = param.token } - /** - * Content/label of the mark (displayed text) - */ + // ─── Data Properties ───────────────────────────────────────────────────────── + + /** Displayed text of the mark */ get content() { return this.#token.content } set content(value: string) { this.#token.content = value - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + this.#emitChange() } - /** - * Value of the mark (hidden data) - */ + /** Data value associated with the mark */ get value() { return this.#token.value } set value(value: string | undefined) { this.#token.value = value ?? '' - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + this.#emitChange() } - /** - * Meta value of the mark - */ + /** Optional metadata for the mark */ get meta() { return this.#token.meta } set meta(value: string | undefined) { - if (value !== undefined) { - this.#token.meta = value - } else { - delete this.#token.meta - } - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + this.#token.meta = value + this.#emitChange() } - /** - * Nesting depth of this mark (0 for root-level marks). - * Computed lazily on access - O(n) traversal. - */ + // ─── Navigation Properties ─────────────────────────────────────────────────── + + /** Nesting depth (0 for root-level marks) */ get depth(): number { return calculateDepth(this.#token, this.#store.tokens) } - /** - * Whether this mark has nested children - */ + /** Whether this mark has nested children */ get hasChildren(): boolean { return this.#token.children.length > 0 } - /** - * Parent mark token (undefined for root-level marks). - * Computed lazily on access - O(n) traversal. - */ + /** Parent mark token (undefined for root-level marks) */ get parent(): MarkToken | undefined { return findParent(this.#token, this.#store.tokens) } - /** - * Array of child tokens (read-only) - */ + /** Child tokens of this mark */ get tokens(): Token[] { return this.#token.children } - /** - * Change mark content, value, and/or meta at once. - */ + // ─── Mutation Methods ──────────────────────────────────────────────────────── + + /** Update multiple properties in a single operation */ change = (props: {content: string; value?: string; meta?: string}) => { this.#token.content = props.content this.#token.value = props.value ?? '' if (props.meta !== undefined) { this.#token.meta = props.meta } - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + this.#emitChange() } - /** - * Remove this mark. - */ + /** Delete this mark from the editor */ remove = () => this.#store.bus.send(SystemEvent.Delete, {token: this.#token}) + + // ─── Private ───────────────────────────────────────────────────────────────── + + #emitChange(): void { + this.#store.bus.send(SystemEvent.Change, {node: this.#token}) + } } -/** - * Calculate the nesting depth of a token in the tree. - * O(n) traversal - only called when depth is accessed. - */ +// ─── Helper Functions ──────────────────────────────────────────────────────── + function calculateDepth(token: MarkToken, tokens: Token[]): number { let depth = 0 + findDepthRecursive(tokens, 0) + return depth function findDepthRecursive(currentTokens: Token[], currentDepth: number): boolean { for (const t of currentTokens) { @@ -125,18 +112,13 @@ function calculateDepth(token: MarkToken, tokens: Token[]): number { } return false } - - findDepthRecursive(tokens, 0) - return depth } -/** - * Find the parent MarkToken of a given token. - * O(n) traversal - only called when parent is accessed. - */ function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { let parent: MarkToken | undefined - + findParentRecursive(tokens) + return parent + function findParentRecursive(currentTokens: Token[], currentParent?: MarkToken): boolean { for (const t of currentTokens) { if (t === token) { @@ -152,7 +134,4 @@ function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { } return false } - - findParentRecursive(tokens) - return parent } From 5fa9a7dd4b3fe2186c0c6d12f44bcfbf182f0a31 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:20:29 +0300 Subject: [PATCH 07/13] Refactor MarkHandler to utilize findToken utility for depth and parent retrieval - Replaced the calculateDepth and findParent functions with a new findToken utility to streamline the retrieval of depth and parent mark tokens. - Improved the MarkHandler class by enhancing the efficiency of depth and parent property calculations. - Removed redundant helper functions, simplifying the code structure and improving maintainability. --- .../markput/src/lib/classes/MarkHandler.ts | 51 ++----------------- packages/markput/src/lib/utils/findToken.ts | 21 ++++++++ 2 files changed, 24 insertions(+), 48 deletions(-) create mode 100644 packages/markput/src/lib/utils/findToken.ts diff --git a/packages/markput/src/lib/classes/MarkHandler.ts b/packages/markput/src/lib/classes/MarkHandler.ts index ec265298..de184ee3 100644 --- a/packages/markput/src/lib/classes/MarkHandler.ts +++ b/packages/markput/src/lib/classes/MarkHandler.ts @@ -1,6 +1,7 @@ import type {RefObject} from 'react' import type {MarkToken, Store, Token} from '@markput/core' import {SystemEvent} from '@markput/core' +import {findToken} from '../utils/findToken' export class MarkHandler { readonly ref: RefObject @@ -50,7 +51,7 @@ export class MarkHandler { /** Nesting depth (0 for root-level marks) */ get depth(): number { - return calculateDepth(this.#token, this.#store.tokens) + return findToken(this.#store.tokens, this.#token)!.depth } /** Whether this mark has nested children */ @@ -60,7 +61,7 @@ export class MarkHandler { /** Parent mark token (undefined for root-level marks) */ get parent(): MarkToken | undefined { - return findParent(this.#token, this.#store.tokens) + return findToken(this.#store.tokens, this.#token)?.parent } /** Child tokens of this mark */ @@ -89,49 +90,3 @@ export class MarkHandler { this.#store.bus.send(SystemEvent.Change, {node: this.#token}) } } - -// ─── Helper Functions ──────────────────────────────────────────────────────── - -function calculateDepth(token: MarkToken, tokens: Token[]): number { - let depth = 0 - findDepthRecursive(tokens, 0) - return depth - - function findDepthRecursive(currentTokens: Token[], currentDepth: number): boolean { - for (const t of currentTokens) { - if (t === token) { - depth = currentDepth - return true - } - - if (t.type === 'mark' && t.children.length > 0) { - if (findDepthRecursive(t.children, currentDepth + 1)) { - return true - } - } - } - return false - } -} - -function findParent(token: MarkToken, tokens: Token[]): MarkToken | undefined { - let parent: MarkToken | undefined - findParentRecursive(tokens) - return parent - - function findParentRecursive(currentTokens: Token[], currentParent?: MarkToken): boolean { - for (const t of currentTokens) { - if (t === token) { - parent = currentParent - return true - } - - if (t.type === 'mark' && t.children.length > 0) { - if (findParentRecursive(t.children, t)) { - return true - } - } - } - return false - } -} diff --git a/packages/markput/src/lib/utils/findToken.ts b/packages/markput/src/lib/utils/findToken.ts new file mode 100644 index 00000000..7a112e3b --- /dev/null +++ b/packages/markput/src/lib/utils/findToken.ts @@ -0,0 +1,21 @@ +import type {MarkToken, Token} from '@markput/core' + +export interface TokenContext { + depth: number + parent?: MarkToken +} + +export function findToken( + tokens: Token[], + target: Token, + depth = 0, + parent?: MarkToken +): TokenContext | undefined { + for (const token of tokens) { + if (token === target) return {depth, parent} + if (token.type === 'mark') { + const result = findToken(token.children, target, depth + 1, token) + if (result) return result + } + } +} From b7240774465d3084d0c09d00e3fdce9bef75463f Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:22:41 +0300 Subject: [PATCH 08/13] rename functions to utils --- packages/markput/index.ts | 2 +- packages/markput/src/components/Container.tsx | 2 +- packages/markput/src/components/EditableSpan.tsx | 2 +- packages/markput/src/components/StoreProvider.tsx | 2 +- packages/markput/src/components/TextSpan.tsx | 2 +- packages/markput/src/lib/providers/TokenProvider.ts | 2 +- packages/markput/src/lib/{functions => utils}/createContext.ts | 0 .../markput/src/lib/{functions => utils}/createMarkedInput.ts | 0 packages/markput/src/lib/{functions => utils}/dataAttributes.ts | 0 packages/markput/src/lib/{functions => utils}/resolveSlot.ts | 0 10 files changed, 6 insertions(+), 6 deletions(-) rename packages/markput/src/lib/{functions => utils}/createContext.ts (100%) rename packages/markput/src/lib/{functions => utils}/createMarkedInput.ts (100%) rename packages/markput/src/lib/{functions => utils}/dataAttributes.ts (100%) rename packages/markput/src/lib/{functions => utils}/resolveSlot.ts (100%) diff --git a/packages/markput/index.ts b/packages/markput/index.ts index ebf9c31a..d645c18e 100644 --- a/packages/markput/index.ts +++ b/packages/markput/index.ts @@ -1,5 +1,5 @@ export {MarkedInput} from './src/components/MarkedInput' -export {createMarkedInput} from './src/lib/functions/createMarkedInput' +export {createMarkedInput} from './src/lib/utils/createMarkedInput' export {useMark} from './src/lib/hooks/useMark' export {useOverlay} from './src/lib/hooks/useOverlay' export {useListener} from './src/lib/hooks/useListener' diff --git a/packages/markput/src/components/Container.tsx b/packages/markput/src/components/Container.tsx index b5b0d6d6..a456799e 100644 --- a/packages/markput/src/components/Container.tsx +++ b/packages/markput/src/components/Container.tsx @@ -1,5 +1,5 @@ import {memo} from 'react' -import {resolveSlot, resolveSlotProps} from '../lib/functions/resolveSlot' +import {resolveSlot, resolveSlotProps} from '../lib/utils/resolveSlot' import {useListener} from '../lib/hooks/useListener' import {useStore} from '../lib/hooks/useStore' import {Token} from './Token' diff --git a/packages/markput/src/components/EditableSpan.tsx b/packages/markput/src/components/EditableSpan.tsx index 608ccd44..25c85507 100644 --- a/packages/markput/src/components/EditableSpan.tsx +++ b/packages/markput/src/components/EditableSpan.tsx @@ -1,5 +1,5 @@ import type {ClipboardEvent} from 'react' -import {resolveSlot, resolveSlotProps} from '../lib/functions/resolveSlot' +import {resolveSlot, resolveSlotProps} from '../lib/utils/resolveSlot' import {useMark} from '../lib/hooks/useMark' import {useStore} from '../lib/hooks/useStore' diff --git a/packages/markput/src/components/StoreProvider.tsx b/packages/markput/src/components/StoreProvider.tsx index c693f296..e832e87e 100644 --- a/packages/markput/src/components/StoreProvider.tsx +++ b/packages/markput/src/components/StoreProvider.tsx @@ -3,7 +3,7 @@ import {useEffect, useState} from 'react' import {Store, DEFAULT_CLASS_NAME} from '@markput/core' import type {MarkedInputProps} from './MarkedInput' import {StoreContext} from '../lib/providers/StoreContext' -import {mergeClassNames, mergeStyles} from '../lib/functions/resolveSlot' +import {mergeClassNames, mergeStyles} from '../lib/utils/resolveSlot' import {DEFAULT_OPTIONS} from '../constants' interface StoreProviderProps { diff --git a/packages/markput/src/components/TextSpan.tsx b/packages/markput/src/components/TextSpan.tsx index a913a7b9..54e156ac 100644 --- a/packages/markput/src/components/TextSpan.tsx +++ b/packages/markput/src/components/TextSpan.tsx @@ -1,6 +1,6 @@ import type {ClipboardEvent} from 'react' import {useRef} from 'react' -import {resolveSlot, resolveSlotProps} from '../lib/functions/resolveSlot' +import {resolveSlot, resolveSlotProps} from '../lib/utils/resolveSlot' import {useStore} from '../lib/hooks/useStore' import {useToken} from '../lib/providers/TokenProvider' diff --git a/packages/markput/src/lib/providers/TokenProvider.ts b/packages/markput/src/lib/providers/TokenProvider.ts index cc0268ab..fd567eb7 100644 --- a/packages/markput/src/lib/providers/TokenProvider.ts +++ b/packages/markput/src/lib/providers/TokenProvider.ts @@ -1,4 +1,4 @@ -import {createContext} from '../functions/createContext' +import {createContext} from '../utils/createContext' import type {Token} from '@markput/core' export const [useToken, TokenProvider] = createContext('NodeProvider') diff --git a/packages/markput/src/lib/functions/createContext.ts b/packages/markput/src/lib/utils/createContext.ts similarity index 100% rename from packages/markput/src/lib/functions/createContext.ts rename to packages/markput/src/lib/utils/createContext.ts diff --git a/packages/markput/src/lib/functions/createMarkedInput.ts b/packages/markput/src/lib/utils/createMarkedInput.ts similarity index 100% rename from packages/markput/src/lib/functions/createMarkedInput.ts rename to packages/markput/src/lib/utils/createMarkedInput.ts diff --git a/packages/markput/src/lib/functions/dataAttributes.ts b/packages/markput/src/lib/utils/dataAttributes.ts similarity index 100% rename from packages/markput/src/lib/functions/dataAttributes.ts rename to packages/markput/src/lib/utils/dataAttributes.ts diff --git a/packages/markput/src/lib/functions/resolveSlot.ts b/packages/markput/src/lib/utils/resolveSlot.ts similarity index 100% rename from packages/markput/src/lib/functions/resolveSlot.ts rename to packages/markput/src/lib/utils/resolveSlot.ts From cc96d2b7cb915ef87302cfc2624211b214460e90 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:26:55 +0300 Subject: [PATCH 09/13] Refactor Token component to simplify rendering logic and improve type safety - Removed separate MarkTokenComponent and TextTokenComponent, consolidating rendering logic within the Token component. - Enhanced type safety by directly using TokenProvider for both mark and text rendering. - Streamlined the component structure for better readability and maintainability. --- packages/markput/src/components/Token.tsx | 71 ++++------------------- 1 file changed, 10 insertions(+), 61 deletions(-) diff --git a/packages/markput/src/components/Token.tsx b/packages/markput/src/components/Token.tsx index 264fa054..db2b5abc 100644 --- a/packages/markput/src/components/Token.tsx +++ b/packages/markput/src/components/Token.tsx @@ -1,80 +1,29 @@ import {memo} from 'react' -import type {MarkToken, TextToken, Token as TokenType} from '@markput/core' +import type {Token as TokenType} from '@markput/core' import {TokenProvider} from '../lib/providers/TokenProvider' // eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token → Piece → Token import {Piece} from './Piece' import {TextSpan} from './TextSpan' -/** - * Token component - renders a single token (text or mark) with recursive support for nested marks - * - * This component discriminates token types and delegates to type-safe sub-components: - * - MarkToken → MarkTokenComponent (renders Piece with custom Mark component) - * - TextToken → TextTokenComponent (renders TextSpan or plain text) - * - * Type discrimination happens here at the top level, allowing sub-components - * to have compile-time type safety without runtime checks. - * - * The isNested prop determines editing behavior for TextTokens: - * - isNested=false (default): TextTokens are editable contentEditable spans - * - isNested=true: TextTokens render as plain text within nested marks - * - * The component is memoized for performance. - */ +/** Renders a token - marks via Piece, text via TextSpan or plain text if nested */ export const Token = memo(({mark, isNested = false}: {mark: TokenType; isNested?: boolean}) => { - // Type discrimination at top level for compile-time safety in sub-components if (mark.type === 'mark') { - return + return ( + + + + ) } - return -}) - -Token.displayName = 'Token' - -/** - * MarkTokenComponent - renders a MarkToken with type safety - * - * This component: - * - Accepts only MarkToken type (compile-time safety) - * - Wraps content in TokenProvider for context access - * - Delegates rendering to Piece component - * - * The component is memoized for performance. - */ -const MarkTokenComponent = memo(({token}: {token: MarkToken}) => ( - - - -)) - -MarkTokenComponent.displayName = 'MarkTokenComponent' - -/** - * TextTokenComponent - renders a TextToken with type safety - * - * This component: - * - Accepts only TextToken type (compile-time safety) - * - isNested=true: renders plain text (no context needed) - * - isNested=false: wraps in TokenProvider and renders TextSpan - * - * Performance optimization: - * - Nested text tokens skip TokenProvider since context is never consumed - * - * The component is memoized for performance. - */ -const TextTokenComponent = memo(({token, isNested = false}: {token: TextToken; isNested?: boolean}) => { - // Optimization: nested text tokens don't need context provider - // They render as plain text and no child component consumes the token if (isNested) { - return <>{token.content} + return <>{mark.content} } return ( - + ) }) -TextTokenComponent.displayName = 'TextTokenComponent' +Token.displayName = 'Token' From 7512d72db323bea12f2b0bd52bbddaeba42dd64d Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:33:23 +0300 Subject: [PATCH 10/13] Rename Piece to MarkRenderer --- packages/markput/src/components/{Piece.tsx => MarkRenderer.tsx} | 0 packages/markput/src/components/Token.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/markput/src/components/{Piece.tsx => MarkRenderer.tsx} (100%) diff --git a/packages/markput/src/components/Piece.tsx b/packages/markput/src/components/MarkRenderer.tsx similarity index 100% rename from packages/markput/src/components/Piece.tsx rename to packages/markput/src/components/MarkRenderer.tsx diff --git a/packages/markput/src/components/Token.tsx b/packages/markput/src/components/Token.tsx index db2b5abc..0813c7bf 100644 --- a/packages/markput/src/components/Token.tsx +++ b/packages/markput/src/components/Token.tsx @@ -2,7 +2,7 @@ import {memo} from 'react' import type {Token as TokenType} from '@markput/core' import {TokenProvider} from '../lib/providers/TokenProvider' // eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token → Piece → Token -import {Piece} from './Piece' +import {Piece} from './MarkRenderer' import {TextSpan} from './TextSpan' /** Renders a token - marks via Piece, text via TextSpan or plain text if nested */ From 359c6da6db0d5cce93211cf43ddeae8433472fe4 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:37:12 +0300 Subject: [PATCH 11/13] Refactor MarkRenderer component to streamline child rendering and improve type safety - Renamed Piece component to MarkRenderer for clarity. - Simplified child rendering logic by directly mapping children tokens. - Enhanced type safety by ensuring MarkProps are constructed correctly based on children presence. --- .../markput/src/components/MarkRenderer.tsx | 45 +++---------------- packages/markput/src/components/Token.tsx | 8 ++-- 2 files changed, 10 insertions(+), 43 deletions(-) diff --git a/packages/markput/src/components/MarkRenderer.tsx b/packages/markput/src/components/MarkRenderer.tsx index b0ad39d7..a96b3ea0 100644 --- a/packages/markput/src/components/MarkRenderer.tsx +++ b/packages/markput/src/components/MarkRenderer.tsx @@ -3,59 +3,26 @@ import {useStore} from '../lib/hooks/useStore' import {useSlot} from '../lib/hooks/useSlot' import {useToken} from '../lib/providers/TokenProvider' import type {MarkProps} from '../types' -// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Piece → Token → MarkTokenComponent → Piece +// eslint-disable-next-line import/no-cycle import {Token} from './Token' -/** - * Piece component - renders a MarkToken with its custom Mark component - * - * This component: - * 1. Retrieves the MarkToken from context (type-safe via MarkTokenComponent) - * 2. Renders children directly (breaking circular import with useMark) - * 3. Constructs MarkProps (value, meta, nested, children) - * 4. Resolves Mark component and props using useSlot hook - * 5. Passes result to the resolved Mark component - * - * Type safety: - * - Piece is only rendered via MarkTokenComponent which guarantees MarkToken type - * - Runtime check kept for defensive programming but should never trigger - * - * Children rendering: - * - If token.children is empty: children prop is undefined (backward compatible) - * - If token.children has items: recursively renders them as ReactNode - * - * Slot resolution (via useSlot): - * - component: option.slots.mark → global Mark → undefined - * - props: slotProps.mark transformer or direct object, fallback to MarkProps - */ -export function Piece() { - const node = useToken() +/** Renders a MarkToken using the resolved Mark component from useSlot */ +export function MarkRenderer() { + const node = useToken() as MarkToken const {options, key} = useStore(store => ({options: store.props.options, key: store.key}), true) - // Type guard - should never trigger since Piece is only rendered via MarkTokenComponent - // Kept for defensive programming and to satisfy TypeScript - if (node.type !== 'mark') { - throw new Error('Piece component expects a MarkToken') - } - - // Get option and construct base MarkProps const option = options?.[node.descriptor.index] - // Render children directly in component (not in hook) to break circular import const children = - node.children.length > 0 - ? node.children.map(child => ) - : undefined + node.children.map(child => ) const markPropsData: MarkProps = { value: node.value, meta: node.meta, nested: node.nested?.content, - children, + children: node.children.length > 0 ? children : undefined, } - // Resolve Mark component and props with proper fallback chain - // (throws error if Mark component not found) const [Mark, props] = useSlot('mark', option, markPropsData) return diff --git a/packages/markput/src/components/Token.tsx b/packages/markput/src/components/Token.tsx index 0813c7bf..df08b4ed 100644 --- a/packages/markput/src/components/Token.tsx +++ b/packages/markput/src/components/Token.tsx @@ -1,16 +1,16 @@ import {memo} from 'react' import type {Token as TokenType} from '@markput/core' import {TokenProvider} from '../lib/providers/TokenProvider' -// eslint-disable-next-line import/no-cycle -- Legitimate recursive component relationship: Token → Piece → Token -import {Piece} from './MarkRenderer' +// eslint-disable-next-line import/no-cycle +import {MarkRenderer} from './MarkRenderer' import {TextSpan} from './TextSpan' -/** Renders a token - marks via Piece, text via TextSpan or plain text if nested */ +/** Renders a token - marks via MarkRenderer, text via TextSpan or plain text if nested */ export const Token = memo(({mark, isNested = false}: {mark: TokenType; isNested?: boolean}) => { if (mark.type === 'mark') { return ( - + ) } From cd8dc6f4c9a258720b6c714d10947231d8a78f0c Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:39:44 +0300 Subject: [PATCH 12/13] Refactor MarkRenderer component to enhance child rendering consistency - Streamlined the child rendering logic by directly mapping children tokens. - Improved code clarity by removing unnecessary line breaks in the rendering process. --- packages/markput/src/components/MarkRenderer.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/markput/src/components/MarkRenderer.tsx b/packages/markput/src/components/MarkRenderer.tsx index a96b3ea0..92e94773 100644 --- a/packages/markput/src/components/MarkRenderer.tsx +++ b/packages/markput/src/components/MarkRenderer.tsx @@ -13,8 +13,7 @@ export function MarkRenderer() { const option = options?.[node.descriptor.index] - const children = - node.children.map(child => ) + const children = node.children.map(child => ) const markPropsData: MarkProps = { value: node.value, From 770acf39c92b6d871b3d739a5867829613546051 Mon Sep 17 00:00:00 2001 From: Nowely Date: Sun, 1 Feb 2026 15:45:14 +0300 Subject: [PATCH 13/13] Fix lint --- packages/markput/src/components/Container.tsx | 2 +- packages/markput/src/lib/utils/findToken.ts | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/markput/src/components/Container.tsx b/packages/markput/src/components/Container.tsx index a456799e..3accce4c 100644 --- a/packages/markput/src/components/Container.tsx +++ b/packages/markput/src/components/Container.tsx @@ -20,7 +20,7 @@ export const Container = memo(() => { true ) - useListener('input',() => bus.send(SystemEvent.Change),[]) + useListener('input', () => bus.send(SystemEvent.Change), []) return ( diff --git a/packages/markput/src/lib/utils/findToken.ts b/packages/markput/src/lib/utils/findToken.ts index 7a112e3b..408dc388 100644 --- a/packages/markput/src/lib/utils/findToken.ts +++ b/packages/markput/src/lib/utils/findToken.ts @@ -5,12 +5,7 @@ export interface TokenContext { parent?: MarkToken } -export function findToken( - tokens: Token[], - target: Token, - depth = 0, - parent?: MarkToken -): TokenContext | undefined { +export function findToken(tokens: Token[], target: Token, depth = 0, parent?: MarkToken): TokenContext | undefined { for (const token of tokens) { if (token === target) return {depth, parent} if (token.type === 'mark') {