diff --git a/packages/markput/index.ts b/packages/markput/index.ts index a7724b33..d645c18e 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/utils/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/classes/MarkHandler' +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 741f7443..3accce4c 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/utils/resolveSlot' +import {useListener} from '../lib/hooks/useListener' +import {useStore} from '../lib/hooks/useStore' import {Token} from './Token' import {SystemEvent} from '@markput/core' @@ -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/components/EditableSpan.tsx b/packages/markput/src/components/EditableSpan.tsx index ff4cf5a8..25c85507 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/utils/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/MarkRenderer.tsx b/packages/markput/src/components/MarkRenderer.tsx new file mode 100644 index 00000000..92e94773 --- /dev/null +++ b/packages/markput/src/components/MarkRenderer.tsx @@ -0,0 +1,28 @@ +import type {MarkToken} from '@markput/core' +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 +import {Token} from './Token' + +/** 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) + + const option = options?.[node.descriptor.index] + + const children = node.children.map(child => ) + + const markPropsData: MarkProps = { + value: node.value, + meta: node.meta, + nested: node.nested?.content, + children: node.children.length > 0 ? children : undefined, + } + + const [Mark, props] = useSlot('mark', option, markPropsData) + + return +} diff --git a/packages/markput/src/components/Piece.tsx b/packages/markput/src/components/Piece.tsx deleted file mode 100644 index 599cfe1e..00000000 --- a/packages/markput/src/components/Piece.tsx +++ /dev/null @@ -1,64 +0,0 @@ -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' -import type {MarkProps} from '../types' - -/** - * Piece component - renders a MarkToken with its custom Mark component - * - * This component: - * 1. Retrieves the MarkToken from context - * 2. Constructs MarkProps (value, meta, nested, children) - * 3. Recursively renders nested children if present - * 4. Resolves Mark component and props using useSlot hook - * 5. Passes result to the resolved Mark component - * - * 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() - const {options, key} = useStore( - store => ({ - options: store.props.options, - key: store.key, - }), - true - ) - - // Ensure it's a MarkToken - if (node.type !== 'mark') { - throw new Error('Piece component expects a MarkToken') - } - - // 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, - } - - // 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/StoreProvider.tsx b/packages/markput/src/components/StoreProvider.tsx index 8938eb7f..e832e87e 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/utils/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..54e156ac 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/utils/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 185cbccd..df08b4ed 100644 --- a/packages/markput/src/components/Token.tsx +++ b/packages/markput/src/components/Token.tsx @@ -1,35 +1,29 @@ import {memo} from 'react' import type {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 -import {Piece} from './Piece' +import {TokenProvider} from '../lib/providers/TokenProvider' +// eslint-disable-next-line import/no-cycle +import {MarkRenderer} from './MarkRenderer' 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) - * - * The isNested prop determines editing behavior: - * - 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. - */ -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 - ) : ( +/** 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 ( + + + + ) + } + + if (isNested) { + return <>{mark.content} + } + + return ( + - )} - -)) + + ) +}) Token.displayName = 'Token' 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/lib/classes/MarkHandler.ts b/packages/markput/src/lib/classes/MarkHandler.ts new file mode 100644 index 00000000..de184ee3 --- /dev/null +++ b/packages/markput/src/lib/classes/MarkHandler.ts @@ -0,0 +1,92 @@ +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 + 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 + } + + // ─── Data Properties ───────────────────────────────────────────────────────── + + /** Displayed text of the mark */ + get content() { + return this.#token.content + } + + set content(value: string) { + this.#token.content = value + this.#emitChange() + } + + /** Data value associated with the mark */ + get value() { + return this.#token.value + } + + set value(value: string | undefined) { + this.#token.value = value ?? '' + this.#emitChange() + } + + /** Optional metadata for the mark */ + get meta() { + return this.#token.meta + } + + set meta(value: string | undefined) { + this.#token.meta = value + this.#emitChange() + } + + // ─── Navigation Properties ─────────────────────────────────────────────────── + + /** Nesting depth (0 for root-level marks) */ + get depth(): number { + return findToken(this.#store.tokens, this.#token)!.depth + } + + /** Whether this mark has nested children */ + get hasChildren(): boolean { + return this.#token.children.length > 0 + } + + /** Parent mark token (undefined for root-level marks) */ + get parent(): MarkToken | undefined { + return findToken(this.#store.tokens, this.#token)?.parent + } + + /** Child tokens of this mark */ + get tokens(): Token[] { + return this.#token.children + } + + // ─── 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.#emitChange() + } + + /** 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}) + } +} 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/lib/hooks/useMark.tsx b/packages/markput/src/lib/hooks/useMark.tsx new file mode 100644 index 00000000..a3f96280 --- /dev/null +++ b/packages/markput/src/lib/hooks/useMark.tsx @@ -0,0 +1,42 @@ +import type {RefObject} from 'react' +import {useEffect, useRef, useState} from 'react' +import type {MarkToken} from '@markput/core' +import {useToken} from '../providers/TokenProvider' +import {useStore} from './useStore' +import {MarkHandler} from '../classes/MarkHandler' + +export interface MarkOptions { + /** + * @default false + */ + controlled?: boolean +} + +//TODO subscribe on label/value changing +export const useMark = (options: MarkOptions = {}): MarkHandler => { + const store = useStore() + const token = useToken() + const ref = useRef() as unknown as RefObject + + if (token.type !== 'mark') { + throw new Error('useMark can only be used with mark tokens') + } + + const [mark] = useState(() => new MarkHandler({ref, store, token})) + + useUncontrolledInit(ref, options, token) + + //Sync for state + const readOnly = useStore(state => state.props.readOnly) + useEffect(() => { + mark.readOnly = readOnly + }, [readOnly]) + + return mark +} + +function useUncontrolledInit(ref: RefObject, options: MarkOptions, token: MarkToken) { + useEffect(() => { + if (ref.current && !options.controlled) ref.current.textContent = token.content + }, []) +} 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 67% rename from packages/markput/src/utils/providers/TokenProvider.ts rename to packages/markput/src/lib/providers/TokenProvider.ts index cc0268ab..fd567eb7 100644 --- a/packages/markput/src/utils/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/utils/functions/createContext.ts b/packages/markput/src/lib/utils/createContext.ts similarity index 100% rename from packages/markput/src/utils/functions/createContext.ts rename to packages/markput/src/lib/utils/createContext.ts diff --git a/packages/markput/src/utils/functions/createMarkedInput.ts b/packages/markput/src/lib/utils/createMarkedInput.ts similarity index 100% rename from packages/markput/src/utils/functions/createMarkedInput.ts rename to packages/markput/src/lib/utils/createMarkedInput.ts diff --git a/packages/markput/src/utils/functions/dataAttributes.ts b/packages/markput/src/lib/utils/dataAttributes.ts similarity index 100% rename from packages/markput/src/utils/functions/dataAttributes.ts rename to packages/markput/src/lib/utils/dataAttributes.ts diff --git a/packages/markput/src/lib/utils/findToken.ts b/packages/markput/src/lib/utils/findToken.ts new file mode 100644 index 00000000..408dc388 --- /dev/null +++ b/packages/markput/src/lib/utils/findToken.ts @@ -0,0 +1,16 @@ +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 + } + } +} diff --git a/packages/markput/src/utils/functions/resolveSlot.ts b/packages/markput/src/lib/utils/resolveSlot.ts similarity index 100% rename from packages/markput/src/utils/functions/resolveSlot.ts rename to packages/markput/src/lib/utils/resolveSlot.ts diff --git a/packages/markput/src/utils/hooks/useMark.ts b/packages/markput/src/utils/hooks/useMark.ts deleted file mode 100644 index d8c2fc98..00000000 --- a/packages/markput/src/utils/hooks/useMark.ts +++ /dev/null @@ -1,225 +0,0 @@ -import type {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' - -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) - */ - depth: number - /** - * Whether this mark has nested children - */ - hasChildren: boolean - /** - * Parent mark token (undefined for root-level marks) - */ - parent?: MarkToken - /** - * Array of child tokens (read-only) - */ - children: Token[] -} - -export interface MarkOptions { - /** - * @default false - */ - controlled?: boolean -} - -//TODO subscribe on label/value changing -export const useMark = (options: MarkOptions = {}): MarkHandler => { - const store = useStore() - const token = useToken() - const ref = useRef() as unknown as RefObject - - if (token.type !== 'mark') { - throw new Error('useMark can only be used with mark tokens') - } - - const [mark] = useState(() => new MarkHandlerP({ref, store, options, token})) - - useUncontrolledInit(ref, options, token) - - //Sync for state - const readOnly = useStore(state => state.props.readOnly) - useEffect(() => { - 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]) - - // Extend mark with tree navigation properties - mark.depth = depth - mark.hasChildren = token.children.length > 0 - mark.parent = parent - mark.children = token.children - - 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 { - ref: RefObject - readonly #store: Store - readonly #token: MarkToken - - readOnly?: boolean - - // Tree navigation properties (set by useMark hook) - depth: number = 0 - hasChildren: boolean = false - parent?: MarkToken - children: Token[] = [] - - get label() { - return this.#token.content - } - - set label(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 - } - - get value() { - return this.#token.value - } - - set value(value: string | undefined) { - this.#token.value = value ?? '' - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) - } - - 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}) - } - - change = (props: MarkStruct) => { - this.#token.content = props.label - this.#token.value = props.value ?? '' - this.#store.bus.send(SystemEvent.Change, {node: this.#token}) - } - - remove = () => this.#store.bus.send(SystemEvent.Delete, {token: this.#token}) -} - -function useUncontrolledInit(ref: RefObject, options: MarkOptions, token: MarkToken) { - useEffect(() => { - if (ref.current && !options.controlled) ref.current.textContent = 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..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} @@ -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/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 ee81d9c4..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.ts:62](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L62) +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 ce4aa806..a824bef3 100644 --- a/packages/website/src/content/docs/api/interfaces/MarkHandler.md +++ b/packages/website/src/content/docs/api/interfaces/MarkHandler.md @@ -5,153 +5,236 @@ 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) - -## Extends - -- `MarkStruct` +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:5 ## 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.ts:23](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L23) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:10 -Change mark. +*** -#### Parameters +### ref -| 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. | +```ts +readonly ref: RefObject; +``` -#### Returns +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:6 -`void` +## Accessors -*** +### content -### children +#### Get Signature ```ts -children: Token[]; +get content(): string; ``` -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/lib/classes/MarkHandler.ts:21 -Array of child tokens (read-only) +Content/label of the mark (displayed text) + +##### Returns + +`string` + +#### Set Signature + +```ts +set content(value): void; +``` + +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:25 + +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `value` | `string` | + +##### Returns + +`void` *** ### depth +#### Get Signature + ```ts -depth: number; +get 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/lib/classes/MarkHandler.ts:62 + +Nesting depth of this mark (0 for root-level marks). +Computed lazily on access - O(n) traversal. -Nesting depth of this mark (0 for root-level marks) +##### Returns + +`number` *** ### hasChildren +#### Get Signature + ```ts -hasChildren: boolean; +get 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/lib/classes/MarkHandler.ts:69 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.ts:9](https://github.com/Nowely/marked-input/blob/next/packages/markput/src/utils/hooks/useMark.ts#L9) +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:45 + +Meta value of the mark + +##### Returns + +`string` \| `undefined` -#### Inherited from +#### Set Signature ```ts -MarkStruct.label +set meta(value): void; ``` +Defined in: packages/markput/src/lib/classes/MarkHandler.ts:49 + +##### Parameters + +| Parameter | Type | +| ------ | ------ | +| `value` | `string` \| `undefined` | + +##### Returns + +`void` + *** -### meta? +### parent + +#### Get Signature ```ts -optional meta: string; +get parent(): MarkToken | undefined; ``` -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/lib/classes/MarkHandler.ts:77 -Meta value of the mark +Parent mark token (undefined for root-level marks). +Computed lazily on access - O(n) traversal. + +##### Returns + +[`MarkToken`](/api/interfaces/marktoken/) \| `undefined` *** -### parent? +### tokens + +#### Get Signature ```ts -optional parent: MarkToken; +get tokens(): Token[]; ``` -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/lib/classes/MarkHandler.ts:84 + +Array of child tokens (read-only) + +##### Returns -Parent mark token (undefined for root-level marks) +[`Token`](/api/type-aliases/token/)[] *** -### readOnly? +### value + +#### Get Signature ```ts -optional readOnly: boolean; +get value(): string | undefined; ``` -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/lib/classes/MarkHandler.ts:33 -Passed the readOnly prop value +Value of the mark (hidden data) -*** +##### Returns -### ref +`string` \| `undefined` + +#### Set Signature ```ts -ref: RefObject; +set value(value): void; ``` -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/lib/classes/MarkHandler.ts:37 -MarkStruct ref. Used for focusing and key handling operations. +##### Parameters -*** +| Parameter | Type | +| ------ | ------ | +| `value` | `string` \| `undefined` | -### remove() +##### Returns + +`void` + +## Methods + +### change() ```ts -remove: () => void; +change(props): 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/lib/classes/MarkHandler.ts:91 -Remove itself. +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 @@ -159,16 +242,16 @@ Remove itself. *** -### value? +### remove() ```ts -optional value: string; +remove(): void; ``` -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/lib/classes/MarkHandler.ts:103 -#### Inherited from +Remove this mark. -```ts -MarkStruct.value -``` +#### Returns + +`void` 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.