@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.
- package/README.md +82 -1
- package/dist/index.d.ts +326 -2
- package/dist/index.js +552 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +43 -0
- package/src/transformer/BlockTransformer.ts +552 -0
- package/src/transformer/index.ts +36 -0
- package/src/transformer/plugins.ts +113 -0
- package/src/transformer/styles.css +33 -0
- package/src/transformer/types.ts +114 -0
- package/src/transformer/utils.ts +191 -0
|
@@ -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
|
+
}
|