@incremark/core 0.0.4 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Incremark BlockTransformer Animation Styles
3
+ *
4
+ * 这些样式用于配合 BlockTransformer 的 effect 选项
5
+ * 可以直接导入使用,或复制到你的项目中自定义
6
+ *
7
+ * 注意:光标字符已直接内嵌到内容中,以下样式仅供参考
8
+ */
9
+
10
+ /* ============ Typing 打字机光标效果 ============ */
11
+
12
+ /**
13
+ * 容器需要添加 .incremark-typing 类
14
+ * 光标字符已直接添加到正在输入的块内容末尾
15
+ */
16
+ .incremark-typing .incremark-pending {
17
+ /* 光标字符已内嵌在内容中,默认为 | */
18
+ }
19
+
20
+ /* ============ 代码块特殊处理 ============ */
21
+
22
+ /* 代码块内使用等宽字体 */
23
+ .incremark-typing pre,
24
+ .incremark-typing code {
25
+ font-family: monospace;
26
+ }
27
+
28
+ /* ============ 工具类 ============ */
29
+
30
+ /* 平滑过渡 */
31
+ .incremark-smooth * {
32
+ transition: opacity 0.1s ease-out;
33
+ }
@@ -0,0 +1,114 @@
1
+ import type { RootContent } from 'mdast'
2
+
3
+ /**
4
+ * 源 Block 类型(来自解析器)
5
+ */
6
+ export interface SourceBlock<T = unknown> {
7
+ /** 唯一标识 */
8
+ id: string
9
+ /** AST 节点 */
10
+ node: RootContent
11
+ /** 块状态 */
12
+ status: 'pending' | 'stable' | 'completed'
13
+ /** 用户自定义元数据 */
14
+ meta?: T
15
+ }
16
+
17
+ /**
18
+ * 显示用的 Block(转换后)
19
+ */
20
+ export interface DisplayBlock<T = unknown> extends SourceBlock<T> {
21
+ /** 用于显示的 AST 节点(可能是截断的) */
22
+ displayNode: RootContent
23
+ /** 显示进度 0-1 */
24
+ progress: number
25
+ /** 是否已完成显示 */
26
+ isDisplayComplete: boolean
27
+ }
28
+
29
+ /**
30
+ * 动画效果类型
31
+ * - 'none': 无动画效果
32
+ * - 'fade-in': 新增字符渐入效果
33
+ * - 'typing': 打字机光标效果
34
+ */
35
+ export type AnimationEffect = 'none' | 'fade-in' | 'typing'
36
+
37
+ /**
38
+ * Transformer 插件
39
+ */
40
+ export interface TransformerPlugin {
41
+ /** 插件名称 */
42
+ name: string
43
+
44
+ /**
45
+ * 判断是否处理此节点
46
+ * 返回 true 表示这个插件要处理此节点
47
+ */
48
+ match?: (node: RootContent) => boolean
49
+
50
+ /**
51
+ * 自定义字符数计算
52
+ * 返回 undefined 则使用默认逻辑
53
+ * 返回 0 表示立即显示(不参与逐字符效果)
54
+ */
55
+ countChars?: (node: RootContent) => number | undefined
56
+
57
+ /**
58
+ * 自定义截断逻辑
59
+ * @param node 原始节点
60
+ * @param displayedChars 当前应显示的字符数
61
+ * @param totalChars 该节点的总字符数
62
+ * @returns 截断后的节点,null 表示不显示
63
+ */
64
+ sliceNode?: (
65
+ node: RootContent,
66
+ displayedChars: number,
67
+ totalChars: number
68
+ ) => RootContent | null
69
+
70
+ /**
71
+ * 节点显示完成时的回调
72
+ */
73
+ onComplete?: (node: RootContent) => void
74
+ }
75
+
76
+ /**
77
+ * Transformer 配置选项
78
+ */
79
+ export interface TransformerOptions {
80
+ /**
81
+ * 每 tick 增加的字符数
82
+ * - number: 固定步长(默认 1)
83
+ * - [min, max]: 随机步长区间(更自然的打字效果)
84
+ */
85
+ charsPerTick?: number | [number, number]
86
+ /** tick 间隔 (ms),默认 20 */
87
+ tickInterval?: number
88
+ /** 动画效果,默认 'none' */
89
+ effect?: AnimationEffect
90
+ /** 插件列表 */
91
+ plugins?: TransformerPlugin[]
92
+ /** 状态变化回调 */
93
+ onChange?: (displayBlocks: DisplayBlock[]) => void
94
+ /**
95
+ * 是否在页面不可见时自动暂停
96
+ * 默认 true,节省资源
97
+ */
98
+ pauseOnHidden?: boolean
99
+ }
100
+
101
+ /**
102
+ * Transformer 内部状态
103
+ */
104
+ export interface TransformerState<T = unknown> {
105
+ /** 已完成显示的 blocks */
106
+ completedBlocks: SourceBlock<T>[]
107
+ /** 当前正在显示的 block */
108
+ currentBlock: SourceBlock<T> | null
109
+ /** 当前 block 已显示的字符数 */
110
+ currentProgress: number
111
+ /** 等待显示的 blocks */
112
+ pendingBlocks: SourceBlock<T>[]
113
+ }
114
+
@@ -0,0 +1,191 @@
1
+ import type { RootContent, Text, Parent } from 'mdast'
2
+
3
+ /**
4
+ * 文本块片段(用于渐入动画)
5
+ */
6
+ export interface TextChunk {
7
+ /** 文本内容 */
8
+ text: string
9
+ /** 创建时间戳 */
10
+ createdAt: number
11
+ }
12
+
13
+ /**
14
+ * 扩展的文本节点(支持 chunks)
15
+ */
16
+ export interface TextNodeWithChunks extends Text {
17
+ /** 稳定部分的长度(不需要动画) */
18
+ stableLength?: number
19
+ /** 临时的文本片段,用于渐入动画 */
20
+ chunks?: TextChunk[]
21
+ }
22
+
23
+ /**
24
+ * AST 节点的通用类型(文本节点或容器节点)
25
+ */
26
+ interface AstNode {
27
+ type: string
28
+ value?: string
29
+ children?: AstNode[]
30
+ }
31
+
32
+ /**
33
+ * 计算 AST 节点的总字符数
34
+ */
35
+ export function countChars(node: RootContent): number {
36
+ let count = 0
37
+
38
+ function traverse(n: AstNode): void {
39
+ if (n.value && typeof n.value === 'string') {
40
+ count += n.value.length
41
+ return
42
+ }
43
+ if (n.children && Array.isArray(n.children)) {
44
+ for (const child of n.children) {
45
+ traverse(child)
46
+ }
47
+ }
48
+ }
49
+
50
+ traverse(node as AstNode)
51
+ return count
52
+ }
53
+
54
+ /**
55
+ * 累积的 chunks 信息
56
+ */
57
+ export interface AccumulatedChunks {
58
+ /** 已经稳定显示的字符数(不需要动画) */
59
+ stableChars: number
60
+ /** 累积的 chunk 列表 */
61
+ chunks: TextChunk[]
62
+ }
63
+
64
+ /** chunk 范围信息 */
65
+ interface ChunkRange {
66
+ start: number
67
+ end: number
68
+ chunk: TextChunk
69
+ }
70
+
71
+ /**
72
+ * 截断 AST 节点,只保留前 maxChars 个字符
73
+ * 支持 chunks(用于渐入动画)
74
+ *
75
+ * @param node 原始节点
76
+ * @param maxChars 最大字符数
77
+ * @param accumulatedChunks 累积的 chunks 信息(用于渐入动画)
78
+ * @returns 截断后的节点,如果 maxChars <= 0 返回 null
79
+ */
80
+ export function sliceAst(
81
+ node: RootContent,
82
+ maxChars: number,
83
+ accumulatedChunks?: AccumulatedChunks
84
+ ): RootContent | null {
85
+ if (maxChars <= 0) return null
86
+
87
+ let remaining = maxChars
88
+ let charIndex = 0
89
+
90
+ // 计算 chunks 在文本中的范围
91
+ const chunkRanges: ChunkRange[] = []
92
+ if (accumulatedChunks && accumulatedChunks.chunks.length > 0) {
93
+ let chunkStart = accumulatedChunks.stableChars
94
+ for (const chunk of accumulatedChunks.chunks) {
95
+ chunkRanges.push({
96
+ start: chunkStart,
97
+ end: chunkStart + chunk.text.length,
98
+ chunk
99
+ })
100
+ chunkStart += chunk.text.length
101
+ }
102
+ }
103
+
104
+ function process(n: AstNode): AstNode | null {
105
+ if (remaining <= 0) return null
106
+
107
+ // 文本类节点:截断 value,可能添加 chunks
108
+ if (n.value && typeof n.value === 'string') {
109
+ const take = Math.min(n.value.length, remaining)
110
+ remaining -= take
111
+ if (take === 0) return null
112
+
113
+ const slicedValue = n.value.slice(0, take)
114
+ const nodeStart = charIndex
115
+ const nodeEnd = charIndex + take
116
+ charIndex += take
117
+
118
+ const result: AstNode & { stableLength?: number; chunks?: TextChunk[] } = {
119
+ ...n,
120
+ value: slicedValue
121
+ }
122
+
123
+ // 检查是否有 chunks 落在这个节点范围内
124
+ if (chunkRanges.length > 0 && accumulatedChunks) {
125
+ const nodeChunks: TextChunk[] = []
126
+ let firstChunkLocalStart = take // 第一个 chunk 在节点中的起始位置
127
+
128
+ for (const range of chunkRanges) {
129
+ // 计算 chunk 与当前节点的交集
130
+ const overlapStart = Math.max(range.start, nodeStart)
131
+ const overlapEnd = Math.min(range.end, nodeEnd)
132
+
133
+ if (overlapStart < overlapEnd) {
134
+ // 有交集,提取对应的文本
135
+ const localStart = overlapStart - nodeStart
136
+ const localEnd = overlapEnd - nodeStart
137
+ const chunkText = slicedValue.slice(localStart, localEnd)
138
+
139
+ if (chunkText.length > 0) {
140
+ // 记录第一个 chunk 的起始位置
141
+ if (nodeChunks.length === 0) {
142
+ firstChunkLocalStart = localStart
143
+ }
144
+ nodeChunks.push({
145
+ text: chunkText,
146
+ createdAt: range.chunk.createdAt
147
+ })
148
+ }
149
+ }
150
+ }
151
+
152
+ if (nodeChunks.length > 0) {
153
+ result.stableLength = firstChunkLocalStart
154
+ result.chunks = nodeChunks
155
+ }
156
+ }
157
+
158
+ return result
159
+ }
160
+
161
+ // 容器节点:递归处理 children
162
+ if (n.children && Array.isArray(n.children)) {
163
+ const newChildren: AstNode[] = []
164
+ for (const child of n.children) {
165
+ if (remaining <= 0) break
166
+ const processed = process(child)
167
+ if (processed) {
168
+ newChildren.push(processed)
169
+ }
170
+ }
171
+ if (newChildren.length === 0) {
172
+ return null
173
+ }
174
+ return { ...n, children: newChildren }
175
+ }
176
+
177
+ // 其他节点(如 thematicBreak, image)
178
+ remaining -= 1
179
+ charIndex += 1
180
+ return { ...n }
181
+ }
182
+
183
+ return process(node as AstNode) as RootContent | null
184
+ }
185
+
186
+ /**
187
+ * 深拷贝 AST 节点
188
+ */
189
+ export function cloneNode<T extends RootContent>(node: T): T {
190
+ return JSON.parse(JSON.stringify(node))
191
+ }