@anyblock/any-block-core 3.4.6

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,497 @@
1
+ /**
2
+ * 处理器_两列列表版
3
+ *
4
+ * - md_str <-> 列表数据
5
+ * - 列表数据 <-> html
6
+ * - 表格数据 -> 列表数据
7
+ */
8
+
9
+ import { ABCSetting, ABReg } from '../ABSetting'
10
+ import {ABConvert_IOEnum, ABConvert, type ABConvert_SpecSimp} from "./ABConvert"
11
+ import {ABConvertManager} from "../ABConvertManager"
12
+ import type {ListItem, List_ListItem} from "./abc_list"
13
+
14
+
15
+ /**
16
+ * 二列列表,特征是level只有0和1
17
+ *
18
+ * @detail
19
+ * 通常用于:
20
+ * - 二层一叉树
21
+ * - dirTree的初处理
22
+ * - Tabs
23
+ * - TimeLine
24
+ * - 仿列表
25
+ * - ...
26
+ * - 二层多叉树
27
+ * - fc-table (首列重要树)
28
+ * - ...
29
+ *
30
+ * 特别是对于mdit-container的 `:::+@` 来说,这种方式是更为方便的
31
+ */
32
+ export interface C2ListItem extends ListItem {
33
+ level: 0|1;
34
+ }[]
35
+ export type List_C2ListItem = C2ListItem[]
36
+
37
+ /// 一些列表相关的工具集
38
+ export class C2ListProcess{
39
+
40
+ // ----------------------- str -> listData ------------------------
41
+
42
+ /**
43
+ * 多层树转二层一叉树
44
+ *
45
+ * @detail
46
+ * 特点是level只有0和1两种
47
+ *
48
+ * example:
49
+ * - 1
50
+ * - 2
51
+ * - 3
52
+ * - 2
53
+ * to:
54
+ * - 1
55
+ * - 2\n - 3\n - 2
56
+ */
57
+ static data_mL_2_2L1B(
58
+ list_itemInfo: List_ListItem
59
+ ): List_C2ListItem{
60
+ const list_itemInfo2: List_C2ListItem = []
61
+ const level1:0 = 0
62
+ const level2:1 = 1
63
+ let flag_leve2 = false // 表示触发过level2,当遇到level1会重置
64
+ for (const itemInfo of list_itemInfo) {
65
+ if (level1>=itemInfo.level){ // 是level1
66
+ list_itemInfo2.push({
67
+ content: itemInfo.content.trim(),
68
+ level: level1
69
+ })
70
+ flag_leve2 = false
71
+ continue
72
+ }
73
+ if (true){ // 是level2/level2+/level2-
74
+ if (!flag_leve2){ // 新建
75
+ list_itemInfo2.push({
76
+ content: itemInfo.content.trim(),
77
+ level: level2
78
+ })
79
+ flag_leve2 = true
80
+ continue
81
+ }
82
+ else { // 内换行
83
+ const old_itemInfo = list_itemInfo2.pop()
84
+ if(old_itemInfo){
85
+ let new_content = itemInfo.content.trim()
86
+ if (itemInfo.level>level2) new_content = "- "+new_content
87
+ for (let i=0; i<(itemInfo.level-level2); i++) new_content = " "+new_content;
88
+ new_content = old_itemInfo.content+"\n"+new_content
89
+ list_itemInfo2.push({
90
+ content: new_content,
91
+ level: level2
92
+ })
93
+ }
94
+ }
95
+ }
96
+ }
97
+ return list_itemInfo2
98
+ }
99
+
100
+ /**
101
+ * 多层树转二层树
102
+ *
103
+ * @detail
104
+ * 特点是level只有0和1两种
105
+ *
106
+ * example:
107
+ * - 1
108
+ * - 2
109
+ * - 3
110
+ * - 2
111
+ * to:
112
+ * - 1
113
+ * - 2\n - 3
114
+ * - 2
115
+ */
116
+ static data_mL_2_2L(
117
+ list_itemInfo: List_ListItem
118
+ ): List_C2ListItem{
119
+ const list_itemInfo2: List_C2ListItem = []
120
+ const level1:0 = 0
121
+ const level2:1 = 1
122
+ for (const itemInfo of list_itemInfo) {
123
+ if (level1>=itemInfo.level){ // 是level1
124
+ list_itemInfo2.push({
125
+ content: itemInfo.content.trim(),
126
+ level: level1
127
+ })
128
+ continue
129
+ }
130
+ if (level2>=itemInfo.level){ // 是level2/level2-
131
+ list_itemInfo2.push({
132
+ content: itemInfo.content.trim(),
133
+ level: level2
134
+ })
135
+ continue
136
+ }
137
+ else{ // level2+,内换行 //
138
+ const old_itemInfo = list_itemInfo2.pop()
139
+ if(old_itemInfo){
140
+ let new_content = itemInfo.content.trim()
141
+ if (itemInfo.level>level2) new_content = "- "+new_content
142
+ for (let i=0; i<(itemInfo.level-level2); i++) new_content = " "+new_content;
143
+ new_content = old_itemInfo.content+"\n"+new_content
144
+ list_itemInfo2.push({
145
+ content: new_content,
146
+ level: level2
147
+ })
148
+ }
149
+ }
150
+ }
151
+ return list_itemInfo2
152
+
153
+ /*
154
+ let list_itemInfo2: {content: string;level: number;}[] = []
155
+ let level1 = -1
156
+ let level2 = -1
157
+ for (let itemInfo of list_itemInfo) {
158
+ let this_level: number // 一共三种可能:0、1、(1+)表
159
+ if (level1<0) {level1=itemInfo.level; this_level = level1} // 未配置level1
160
+ else if (level1>=itemInfo.level) this_level = level1 // 是level1
161
+ else if (level2<0) {level2=itemInfo.level; this_level = level2} // 未配置level2
162
+ else if (level2>=itemInfo.level) this_level = level2 // 是level2
163
+ else { // (level2<itemInfo.level) // 依然是level2,但进行内换行,并把列表符和缩进给加回去
164
+ let old_itemInfo = list_itemInfo2.pop()
165
+ if(old_itemInfo){
166
+ let new_content = "- "+itemInfo.content.trim()
167
+ for (let i=0; i<(itemInfo.level-level2); i++) new_content = " "+new_content;
168
+ new_content = old_itemInfo.content+"\n"+new_content
169
+ list_itemInfo2.push({
170
+ content: new_content,
171
+ level: level2
172
+ })
173
+ }
174
+ continue
175
+ }
176
+ list_itemInfo2.push({
177
+ content: itemInfo.content.trim(),
178
+ level: level2
179
+ })
180
+ }
181
+ console.log("前后数据", list_itemInfo, list_itemInfo2)
182
+ return list_itemInfo2*/
183
+ }
184
+
185
+ /**
186
+ * 列表转二列列表数据
187
+ *
188
+ * @detail
189
+ * 为什么要直接转,而不能list2data|data2c2data来复用
190
+ * 因为会损失信息
191
+ *
192
+ * @param text 列表的md文本
193
+ * @param modeG 识别内换行符号。内换行符被替换为换行+前缀,比listdata的处理简单些,后面不需要再提高level // TODO 这部分逻辑应该抽离出来做成独立的处理器
194
+ */
195
+ static list2c2data(text: string, modeG=true){
196
+ const list_itemInfo:List_C2ListItem = []
197
+ const list_text = text.trimStart().split("\n")
198
+
199
+ // 获取最大的标题级别
200
+ const first_match = list_text[0].match(ABReg.reg_list_noprefix)
201
+ if (!first_match || first_match[1]) {
202
+ console.error("不是列表内容:", list_text[0])
203
+ return list_itemInfo
204
+ }
205
+ const root_list_level:number = first_match[1].length // 第一个列表(也是缩进最小)的 `- ` 前空格数
206
+
207
+ // 循环填充
208
+ let current_content:string = "" // level1的子内容
209
+ let current_content_prefix:string = "" // level1的子内容的前缀
210
+ for (let line of list_text) {
211
+ const match_list = line.match(ABReg.reg_list_noprefix)
212
+ if (match_list && !match_list[1] && match_list[1].length<=root_list_level){ // b1. 遇到同等标题,level0新项出现
213
+ add_current_content()
214
+ let content = match_list[4]
215
+ // 替换掉内换行符
216
+ if (modeG) {
217
+ const inlines = match_list[4].split(ABReg.inline_split)
218
+ if (inlines.length > 1) {
219
+ const second_part = content.indexOf(inlines[1])
220
+ current_content += content.slice(second_part) + "\n"
221
+ current_content_prefix = " " // (TODO风险) 通过内换行符,则新的行前缀为双空格 (用户可能用tab的1和四空格的4,但不影响)
222
+ content = inlines[0]
223
+ }
224
+ }
225
+ // 新项
226
+ list_itemInfo.push({
227
+ content: content,
228
+ level: 0
229
+ })
230
+ } else { // b2. 子内容
231
+ if (current_content.trim()=="") { // 第一行的子内容前缀提取
232
+ if (match_list && match_list[1]) current_content_prefix = match_list[1] // 有 `- `,则为 `- ` 前的字符数
233
+ else current_content_prefix = " " // (TODO风险) 无 `- `,则设前缀字符数为2 (用户可能用tab的1和四空格的4,但少消除2个一般不会影响,主要避免前缀4空格自动转化为缩进代码块)
234
+ }
235
+ if (line.startsWith("\t")) line = line.substring(1);
236
+ else if (line.startsWith(current_content_prefix)) { // 子内容前缀去除
237
+ line = line.substring(current_content_prefix.length);
238
+ }
239
+ current_content += line+"\n" // 子内容拼接
240
+ }
241
+ }
242
+ add_current_content()
243
+ return list_itemInfo
244
+
245
+ function add_current_content(){ // 刷新写入缓存的尾调用
246
+ if (current_content.trim()=="") return
247
+ list_itemInfo.push({
248
+ content: current_content,
249
+ level: 1
250
+ })
251
+ current_content = ""
252
+ }
253
+ }
254
+
255
+ /**
256
+ * 标题大纲转列表数据
257
+ *
258
+ * @detail
259
+ * 为什么要直接转,而不能title2list来复用
260
+ * 因为title2list会损失标题信息
261
+ */
262
+ static title2c2data(text: string){
263
+ const list_itemInfo:List_C2ListItem = []
264
+ const list_text = text.trimStart().split("\n")
265
+
266
+ // 获取最大的标题级别
267
+ const first_match = list_text[0].match(ABReg.reg_heading_noprefix)
268
+ if (!first_match || first_match[1]) {
269
+ console.error("不是标题内容:", list_text[0])
270
+ return list_itemInfo
271
+ }
272
+ const root_title_level:number = first_match[3].length-1 // 第一个标题(也是最大级别的标题的)的`#`的个数
273
+
274
+ // 循环填充
275
+ let current_content:string = ""
276
+ let codeBlockFlag = ''
277
+ for (const line of list_text) {
278
+ // heading和mdit类型 需要跳过代码块内的结束标志
279
+ if (codeBlockFlag == '') {
280
+ const match = line.match(ABReg.reg_code)
281
+ if (match && match[3]) {
282
+ codeBlockFlag = match[1]+match[3]
283
+ current_content += line+"\n"; continue
284
+ }
285
+ }
286
+ else {
287
+ if (line.indexOf(codeBlockFlag) == 0) codeBlockFlag = ''
288
+ current_content += line+"\n"; continue
289
+
290
+ }
291
+ //
292
+ const match_heading = line.match(ABReg.reg_heading_noprefix)
293
+ if (match_heading && !match_heading[1] && (match_heading[3].length-1)<=root_title_level){ // 遇到同等标题
294
+ add_current_content()
295
+ list_itemInfo.push({
296
+ content: match_heading[4],
297
+ level: 0
298
+ })
299
+ } else {
300
+ current_content += line+"\n"
301
+ }
302
+ }
303
+ add_current_content()
304
+ return list_itemInfo
305
+
306
+ function add_current_content(){ // 刷新写入缓存的尾调用
307
+ if (current_content.trim()=="") return
308
+ list_itemInfo.push({
309
+ content: current_content,
310
+ level: 1
311
+ })
312
+ current_content = ""
313
+ }
314
+ }
315
+
316
+ /**
317
+ * 两列列表数据转标签栏
318
+ */
319
+ static c2data2tab(
320
+ list_itemInfo: List_C2ListItem,
321
+ div: HTMLDivElement,
322
+ modeT: boolean
323
+ ){
324
+ const tab = document.createElement("div"); div.appendChild(tab); tab.classList.add("ab-tab-root");
325
+ if (modeT) tab.setAttribute("modeT", "true")
326
+ const nav = document.createElement("div"); tab.appendChild(nav); nav.classList.add("ab-tab-nav");
327
+ const content = document.createElement("div"); tab.appendChild(content); content.classList.add("ab-tab-content")
328
+ let current_dom:HTMLElement|null = null
329
+ for (let i=0; i<list_itemInfo.length; i++){
330
+ const item = list_itemInfo[i]
331
+ // b1. item标题,顺便创建空内容
332
+ if (item.level==0) {
333
+ const nav_item = document.createElement("button"); nav.appendChild(nav_item); nav_item.classList.add("ab-tab-nav-item");
334
+ nav_item.setAttribute("is_activate", i==0?"true":"false");
335
+ nav_item.setAttribute("data_index", i.toString()); // data_index 用于方便从数组中删除
336
+ nav_item.textContent = item.content.slice(0,20);
337
+
338
+ current_dom = document.createElement("div"); content.appendChild(current_dom); current_dom.classList.add("ab-tab-content-item");
339
+ current_dom.setAttribute("style", i==0?"display:block":"display:none"); current_dom.setAttribute("is_activate", i==0?"true":"false");
340
+ }
341
+ // b2. item内容,在空内容的基础上填充
342
+ else if (current_dom) {
343
+ current_dom.setAttribute("data_index", i.toString()); // data_index 用于方便从数组中删除
344
+ ABConvertManager.getInstance().m_renderMarkdownFn(item.content, current_dom)
345
+ current_dom = null
346
+ }
347
+ // b3. item内容之前没有item标题,不合法,跳过
348
+ else {
349
+ continue
350
+ }
351
+ }
352
+
353
+ // 动态部分
354
+ // 元素全部创建完再来绑按钮事件,不然会有问题
355
+ const lis:NodeListOf<HTMLButtonElement> = tab.querySelectorAll(":scope>.ab-tab-nav>.ab-tab-nav-item")
356
+ const contents = tab.querySelectorAll(":scope>.ab-tab-content>.ab-tab-content-item")
357
+ if (lis.length!=contents.length) console.warn("ab-tab-nav-item和ab-tab-content-item的数量不一致")
358
+ for (let i=0; i<lis.length; i++){
359
+ // 1. 二选一,常规绑定
360
+ // ob选用
361
+ if (ABCSetting.env.startsWith("obsidian")) {
362
+ lis[i].onclick = ()=>{
363
+ for (let j=0; j<contents.length; j++){
364
+ lis[j].setAttribute("is_activate", "false")
365
+ contents[j].setAttribute("is_activate", "false")
366
+ contents[j].setAttribute("style", "display:none")
367
+ }
368
+ lis[i].setAttribute("is_activate", "true")
369
+ contents[i].setAttribute("is_activate", "true")
370
+ contents[i].setAttribute("style", "display:block")
371
+ }
372
+ }
373
+ // 2. 二选一,嵌入内联onclick
374
+ // mdit (vuepress、app) 选用
375
+ else {
376
+ lis[i].setAttribute("onclick",`
377
+ const i = ${i}
378
+ const tab_current = this
379
+ const tab_nav = this.parentNode
380
+ const tab_root = tab_nav.parentNode
381
+ const tab_content = tab_root.querySelector(":scope>.ab-tab-content")
382
+ const tab_nav_items = tab_nav.querySelectorAll(":scope>.ab-tab-nav-item")
383
+ const tab_content_items = tab_content.querySelectorAll(":scope>.ab-tab-content-item")
384
+ for (let j=0; j<tab_content_items.length; j++){
385
+ tab_nav_items[j].setAttribute("is_activate", "false")
386
+ tab_content_items[j].setAttribute("is_activate", "false")
387
+ tab_content_items[j].setAttribute("style", "display:none")
388
+ }
389
+ tab_current.setAttribute("is_activate", "true")
390
+ tab_content_items[i].setAttribute("is_activate", "true")
391
+ tab_content_items[i].setAttribute("style", "display:block")
392
+ `)
393
+ }
394
+ }
395
+
396
+ return div
397
+ }
398
+
399
+ /// 将二列列表转 `容器-元素` 结构
400
+ static c2data2items(c2listdata:List_C2ListItem, el:HTMLElement): HTMLElement {
401
+ const el_items = document.createElement("div"); el.appendChild(el_items); el_items.classList.add("ab-items")
402
+ let el_item:HTMLElement|null = null;
403
+ for (const item of c2listdata) {
404
+ // b1. item标题
405
+ if (item.level == 0) {
406
+ el_item = document.createElement("div"); el_items.appendChild(el_item); el_item.classList.add("ab-items-item")
407
+ const el_title = document.createElement("div"); el_item.appendChild(el_title); el_title.classList.add("ab-items-title")
408
+ ABConvertManager.getInstance().m_renderMarkdownFn(item.content, el_title)
409
+ }
410
+ // b2. item内容
411
+ else if (el_item) {
412
+ const el_content = document.createElement("div"); el_item.appendChild(el_content); el_content.classList.add("ab-items-content")
413
+ ABConvertManager.getInstance().m_renderMarkdownFn(item.content, el_content)
414
+ }
415
+ // b3. item内容之前没有item标题,不合法,跳过
416
+ else {
417
+ continue
418
+ }
419
+ }
420
+ return el
421
+ }
422
+ }
423
+
424
+ const _abc_list2c2listdata = ABConvert.factory({
425
+ id: "list2c2listdata",
426
+ name: "列表转c2listdata",
427
+ match: "list2c2listdata",
428
+ default: "list2c2listdata",
429
+ process_param: ABConvert_IOEnum.text,
430
+ process_return: ABConvert_IOEnum.c2list_stream,
431
+ process: (el, header, content: string): List_C2ListItem=>{
432
+ return C2ListProcess.list2c2data(content)
433
+ }
434
+ })
435
+
436
+ const _abc_title2c2listdata = ABConvert.factory({
437
+ id: "title2c2listdata",
438
+ name: "标题转c2listdata",
439
+ match: "title2c2listdata",
440
+ default: "title2c2listdata",
441
+ process_param: ABConvert_IOEnum.text,
442
+ process_return: ABConvert_IOEnum.c2list_stream,
443
+ process: (el, header, content: string): List_C2ListItem=>{
444
+ return C2ListProcess.title2c2data(content)
445
+ }
446
+ })
447
+
448
+ const _abc_c2listdata2tab = ABConvert.factory({
449
+ id: "c2listdata2tab",
450
+ name: "c2listdata转标签",
451
+ match: "c2listdata2tab",
452
+ default: "c2listdata2tab",
453
+ process_param: ABConvert_IOEnum.c2list_stream,
454
+ process_return: ABConvert_IOEnum.el,
455
+ process: (el, header, content: List_C2ListItem): HTMLElement=>{
456
+ return C2ListProcess.c2data2tab(content, el, false)
457
+ }
458
+ })
459
+
460
+ const _abc_c2listdata2items = ABConvert.factory({
461
+ id: "c2listdata2items",
462
+ name: "c2listdata转容器结构",
463
+ match: "c2listdata2items",
464
+ default: "c2listdata2items",
465
+ process_param: ABConvert_IOEnum.c2list_stream,
466
+ process_return: ABConvert_IOEnum.el,
467
+ process: (el, header, content: List_C2ListItem): HTMLElement=>{
468
+ return C2ListProcess.c2data2items(content, el)
469
+ }
470
+ })
471
+
472
+ const _abc_c2listdata2easytimeline = ABConvert.factory({
473
+ id: "c2listdata2easytimeline",
474
+ name: "适配到easy_timeline",
475
+ match: "c2listdata2easytimeline",
476
+ detail: "适配到easy_timeline格式,需要安装easy timeline插件",
477
+ process_param: ABConvert_IOEnum.c2list_stream,
478
+ process_return: ABConvert_IOEnum.text,
479
+ process: (el, header, content: List_C2ListItem): string=>{
480
+ let all_line = ""
481
+ let line = ""
482
+ for (const item of content) {
483
+ if (item.level == 0) {
484
+ if (line != "") all_line += line + '\n\n'
485
+ line = item.content + '. '
486
+ }
487
+ else {
488
+ if (line == "") line = " . "
489
+ line += item.content
490
+ }
491
+ }
492
+ // 尾调用
493
+ if (line != "") all_line += line
494
+
495
+ return "````timeline\n"+all_line+"\n````"
496
+ }
497
+ })
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 转换器_代码块版
3
+ *
4
+ * md_str <-> md_str
5
+ *
6
+ * 和 abc_text 类别类似,区别在于,源/目标数据为代码/代码块
7
+ *
8
+ * 注意该类别内容经常伴随两个关键重要别名一起使用:
9
+ * ```ts
10
+ * {regex: "|list2code|", replacement: "|xList|code(js)|"},
11
+ * {regex: "|code2list|", replacement: "|Xcode|region2indent|addList|"},
12
+ * ```
13
+ */
14
+
15
+ import { ABReg } from "../ABSetting"
16
+ import { ABConvert, ABConvert_IOEnum } from "./ABConvert"
17
+
18
+ const _abc_region2indent = ABConvert.factory({
19
+ id: "region2indent",
20
+ name: "代码注释转缩进",
21
+ detail: "代码块注释转缩进 (识别 `//` 和 `#` 的region注释对),通常配合code2list使用。默认补充两缩进",
22
+ process_param: ABConvert_IOEnum.text,
23
+ process_return: ABConvert_IOEnum.text,
24
+ process: (el, header, content: string): string=>{
25
+ const lists = content.trimEnd().split("\n")
26
+ let newContent = ''
27
+ // let indent_map: {endFlag: string, indent: string}[] = [] // 缩进表。记录停止标识、区块内需要补充的缩进 (弃用,region自身无关缩进,缩进内容固定加层数*两空格)
28
+ // let indent_map: string[] = [] // 缩进表。下标表示在第几层嵌套,内容表示缩进内容
29
+ let startFlagNumber = 0
30
+ const regionReg: RegExp = /^([ \t]*)(#|\/\/)\s*#?(region|endregion)(.*)/
31
+ for (let i=0; i < lists.length; i++) {
32
+ const item = lists[i]
33
+ const match = item.match(regionReg)
34
+
35
+ // b1. 非region项
36
+ if (!match) {
37
+ newContent += '\n' + ' '.repeat(startFlagNumber) + item
38
+ continue
39
+ }
40
+ // b2. region项
41
+ else {
42
+ if (match[3] == 'region') {
43
+ newContent += '\n' + ' '.repeat(startFlagNumber) + match[4].trimStart()
44
+ startFlagNumber++
45
+ }
46
+ else {
47
+ startFlagNumber--
48
+ }
49
+ }
50
+ }
51
+
52
+ return newContent.slice(1) // 去除头部 `\n`
53
+ }
54
+ })
55
+
56
+ const _abc_mdit2code = ABConvert.factory({
57
+ id: "mdit2code",
58
+ name: "mdit转代码块",
59
+ detail: "mdit转代码块 (允许嵌套)。注意 `:*n` 会转化为 `~*(n+3)`, `@aaa bbb` 会转换为 `# bbb` (h1标题)",
60
+ process_param: ABConvert_IOEnum.text,
61
+ process_return: ABConvert_IOEnum.text,
62
+ process: (el, header, content: string): string=>{
63
+ const lists = content.trimEnd().split("\n")
64
+ let newContent = ''
65
+ // 不使用缩进表和缩进计数,不进行语法检查
66
+
67
+ for (let i=0; i < lists.length; i++) {
68
+ const item = lists[i]
69
+ const match = item.match(ABReg.reg_mdit_head)
70
+ const match2 = item.trim().match(/^@(\S*?)\s(.*?)$/)
71
+
72
+ // b1. `^@` 项
73
+ if (match2) {
74
+ newContent += '\n' + '# ' + match2[2]
75
+ }
76
+ // b1. `:::` 项
77
+ else if (match) {
78
+ const flag = '~'.repeat(match[3].length + 3)
79
+ if (match[4].trim() !== "") newContent += '\n' + flag + 'anyblock\n[' + match[4].trimStart() + ']' // 头
80
+ else newContent += '\n' + flag // 尾
81
+ continue
82
+
83
+ }
84
+ // b3. 普通
85
+ else {
86
+ newContent += '\n' + item
87
+ continue
88
+ }
89
+ }
90
+
91
+ return newContent.slice(1) // 去除头部 `\n`
92
+ }
93
+ })