Repository navigation
Expand file tree
/
Copy pathmain.js
More file actions
4498 lines (4207 loc) · 184 KB
/
Copy pathmain.js
File metadata and controls
4498 lines (4207 loc) · 184 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
(function () {
const VIEW_TYPE = "better-footnote-view";
const SAVE_DELAY_MS = 450;
const RENDER_DELAY_MS = 90;
const EDITOR_CHANGE_REFRESH_DELAY_MS = 900;
const CURSOR_SYNC_DELAY_MS = 80;
const FOOTNOTE_CONTINUATION_INDENT = " ";
const FLASH_SELECTION_MS = 1400;
const AUTO_TIDY_DELAY_MS = 250;
const SIDEBAR_JUMP_CURSOR_SUPPRESS_MS = 1200;
// A newly detected footnote triggers automatic Tidy / auto-focus only when the
// note's own editor reported a user transaction (not Obsidian merging an
// external modification) within this window.
const NOTE_EDIT_TRUST_MS = 5000;
// Content cropped by no more than this many pixels (descender zone of the
// last line) is treated as fully visible: no expand arrow, no auto-expand.
const CLIPPED_CONTENT_TOLERANCE_PX = 8;
const DELETED_FOOTNOTE_RESTORE_TTL_MS = 10 * 60 * 1000;
const RESTORED_DELETED_FOOTNOTE_CURSOR_SUPPRESS_MS = 5000;
const MAX_DELETED_FOOTNOTE_RESTORE_RECORDS = 50;
const PLUGIN_ICON = "list-ordered";
const TIDY_FOOTNOTES_PLUGIN_URL = "https://community.obsidian.md/plugins/obsidian-tidy-footnotes";
const DEFAULT_SETTINGS = {
autoTidyAfterNewFootnote: false,
countMode: "auto",
tidyCommandId: "",
renderMarkdownInSidebar: false,
useLivePreviewEditor: true,
};
const COUNT_MODES = new Set(["auto", "characters", "words"]);
const I18N = {
en: {
title: "Better Footnote",
refresh: "Refresh",
noActiveFile: "No active Markdown file",
openMarkdownNote: "Open a Markdown note to edit its footnotes.",
readFailed: "Failed to read file: {message}",
noFootnotes: "No footnote definitions found in this note.",
footnoteCount: "{file} · {count} footnote{plural}",
filteredFootnoteCount: "{file} · {visible}/{total} footnotes · {matches} matches",
searchPlaceholder: "Search footnotes; ^number/name jumps",
searchTooltip: "Search footnote content; type ^ plus a footnote number or name to jump to that footnote, e.g. ^42 or ^citation.",
clearSearch: "Clear",
resumeSearch: "Resume search",
noSearchResults: "No footnotes match your search.",
previousMatch: "Previous match",
nextMatch: "Next match",
searchMatchCount: "{current}/{total}",
multipleReferences: "{count} refs",
referencePosition: "{current}/{total}",
previousReference: "Previous reference",
nextReference: "Next reference",
expandFootnote: "Expand",
collapseFootnote: "Collapse",
definitionButton: "Footnote area",
definitionTooltip: "Jump to the footnote definition area",
saved: "Saved",
saving: "Saving...",
saveError: "Error: {message}",
characters: "{count} character{plural}",
words: "{count} word{plural}",
noActiveFileSave: "No active Markdown file.",
openSourceForReference: "Open the source note to jump to a footnote reference.",
openSourceForDefinition: "Open the source note to jump to a footnote definition.",
noReferenceFound: "No reference found for [^{id}].",
footnoteNotFound: "Footnote [^{id}] was not found.",
saveConflict: "Footnote [^{id}] changed in the note after this card was opened, so this edit was not saved to avoid overwriting it. Your text is kept below:",
saveConflictStatus: "Not saved: note changed",
saveMissing: "Footnote [^{id}] no longer exists in the note (it may have been renumbered or deleted elsewhere), so this edit was not saved. Your text is kept below:",
unreferenced: "Unreferenced",
duplicateNumberBadge: "Number in use",
duplicateNumberHint: "Give this definition a new number in the Footnote area.",
autoTidyPausedDuplicate: "Two footnote definitions share a number, so automatic tidy was paused.",
commandOpen: "Open Better Footnote",
ribbonOpen: "Open Better Footnote",
settingsTitle: "Better Footnote",
countModeName: "Footnote count mode",
countModeDesc: "Auto follows the plugin language: English counts words; Chinese, Japanese, and Korean count characters. Choose a different mode when your writing language differs from the interface language.",
countModeAuto: "Auto",
countModeCharacters: "Characters",
countModeWords: "Words",
renderMarkdownName: "Render Markdown in sidebar",
renderMarkdownDesc: "When enabled, footnote cards you are not editing show rendered Markdown, including links, bold, and other formatting. Click a link to open it. Click the text to edit in an embedded live preview editor that keeps formatting rendered while you type; if it is unavailable, the card falls back to a plain source editor.",
autoTidyName: "Auto tidy after a new footnote",
autoTidyDesc: "Requires Tidy Footnotes. After you insert a new footnote in the note's own editor, Better Footnote runs Tidy Footnotes automatically (footnotes that arrive from Sync or from scripts that write the file directly are only highlighted). This closes Obsidian's built-in floating footnote editor; use the Better Footnote sidebar to edit the footnote.",
tidyInstallName: "Tidy Footnotes integration",
tidyInstallDesc: "Install and enable Tidy Footnotes before using automatic tidying.",
tidyInstallButton: "Open plugin page",
tidyCommandMissing: "Tidy Footnotes command was not found.",
tidyCommandNoEditor: "No Markdown editor is available for Tidy Footnotes.",
tidyCommandFailed: "Failed to run Tidy Footnotes: {message}",
deleteFootnoteMenu: "Delete this footnote",
deleteFootnoteTitle: "Delete footnote [^{id}]?",
deleteFootnoteWithReferences: "This will remove {count} reference marker{plural} and the footnote definition.",
deleteUnreferencedFootnote: "This footnote has no reference markers. This will remove the footnote definition.",
deleteEmptyFootnote: "The footnote definition is empty.",
deleteCancel: "Cancel",
deleteConfirm: "Delete footnote",
deleteNeedsEditor: "Open the source note before deleting a footnote.",
switchToEditingView: "Switch the note to editing view first.",
deleteFailed: "Failed to delete footnote: {message}",
deletedFootnote: "Deleted footnote [^{id}]. Press {shortcut} to undo.",
duplicateFootnoteInserted: "New footnote [^{id}] collides with an existing id. Its marker now references the existing footnote, and an unreferenced duplicate definition was left behind. Consider undoing and using an unused id.",
},
zh: {
title: "Better Footnote",
refresh: "更新",
noActiveFile: "没有打开 Markdown 文件",
openMarkdownNote: "打开一篇 Markdown 笔记后,即可编辑其中的脚注。",
readFailed: "读取文件失败:{message}",
noFootnotes: "这篇笔记中没有找到脚注定义。",
footnoteCount: "{file} · {count} 条脚注",
filteredFootnoteCount: "{file} · 显示 {visible}/{total} 条脚注 · {matches} 处匹配",
searchPlaceholder: "搜索脚注;^编号/名称精准定位",
searchTooltip: "搜索脚注内容;输入 ^ 加脚注编号或名称,可精准定位该条脚注,例如 ^42 或 ^citation。",
clearSearch: "清除",
resumeSearch: "恢复搜索",
noSearchResults: "没有匹配的脚注。",
previousMatch: "上一处匹配",
nextMatch: "下一处匹配",
searchMatchCount: "{current}/{total}",
multipleReferences: "{count} 处引用",
referencePosition: "{current}/{total}",
previousReference: "上一处引用",
nextReference: "下一处引用",
expandFootnote: "展开",
collapseFootnote: "收起",
definitionButton: "脚注区",
definitionTooltip: "跳到文末脚注定义位置",
saved: "已保存",
saving: "保存中...",
saveError: "错误:{message}",
characters: "{count} 字",
words: "{count} 词",
noActiveFileSave: "没有打开 Markdown 文件。",
openSourceForReference: "请先打开源笔记,再跳到脚注引用位置。",
openSourceForDefinition: "请先打开源笔记,再跳到脚注定义位置。",
noReferenceFound: "没有找到 [^{id}] 的正文引用。",
footnoteNotFound: "没有找到脚注 [^{id}]。",
saveConflict: "脚注 [^{id}] 在打开这张卡片之后已在笔记中被修改,为避免覆盖,本次编辑未保存。你的文字保留在下方:",
saveConflictStatus: "未保存:笔记已变更",
saveMissing: "脚注 [^{id}] 已不在笔记中(可能在别处被重新编号或删除),因此本次编辑未保存。你的文字保留在下方:",
unreferenced: "未引用",
duplicateNumberBadge: "编号已占用",
duplicateNumberHint: "请到脚注区给这条定义换一个新编号。",
autoTidyPausedDuplicate: "有编号相同的脚注定义,已暂停自动整理。",
commandOpen: "打开 Better Footnote",
ribbonOpen: "打开 Better Footnote",
settingsTitle: "Better Footnote",
countModeName: "脚注计数方式",
countModeDesc: "自动模式会跟随插件界面语言:英文统计单词数,中文、日文、韩文统计字数。也可以根据写作语言,选择不同于界面语言的计数方式。",
countModeAuto: "自动",
countModeCharacters: "字数",
countModeWords: "单词数",
renderMarkdownName: "侧栏渲染 Markdown",
renderMarkdownDesc: "开启后,未在编辑的脚注卡片会显示渲染后的 Markdown,包括链接、粗体等格式。点击链接直接打开;点击文字进入嵌入式实时预览编辑器,输入时格式保持渲染。若实时预览编辑器不可用,卡片会自动回退为源码编辑框。",
autoTidyName: "新增脚注后自动整理编号",
autoTidyDesc: "需要先安装并启用 Tidy Footnotes。当你在这篇笔记的编辑器里插入新脚注后,Better Footnote 会自动运行 Tidy Footnotes(同步或直接写文件的脚本带来的新脚注只会高亮,不会自动整理)。启用后会关闭 Obsidian 自带的脚注悬浮编辑框,请在 Better Footnote 侧栏中编辑脚注。",
tidyInstallName: "Tidy Footnotes 集成",
tidyInstallDesc: "使用自动整理前,请先安装并启用 Tidy Footnotes。",
tidyInstallButton: "打开插件页面",
tidyCommandMissing: "没有找到 Tidy Footnotes 命令。",
tidyCommandNoEditor: "没有可用于 Tidy Footnotes 的 Markdown 编辑器。",
tidyCommandFailed: "运行 Tidy Footnotes 失败:{message}",
deleteFootnoteMenu: "删除本条脚注",
deleteFootnoteTitle: "删除脚注 [^{id}]?",
deleteFootnoteWithReferences: "将删除 {count} 处引用标记,并删除脚注定义。",
deleteUnreferencedFootnote: "这条脚注没有引用标记。将删除脚注定义。",
deleteEmptyFootnote: "这条脚注定义为空。",
deleteCancel: "取消",
deleteConfirm: "删除脚注",
deleteNeedsEditor: "请先打开对应笔记,再删除脚注。",
switchToEditingView: "请先把笔记切换到编辑视图。",
deleteFailed: "删除脚注失败:{message}",
deletedFootnote: "已删除脚注 [^{id}]。按 {shortcut} 可撤销。",
duplicateFootnoteInserted: "新脚注编号 [^{id}] 与现有脚注撞号:新标记已成为现有脚注的又一处引用,并留下一条未引用的重复定义。建议撤销后改用未使用的编号。",
},
ja: {
title: "Better Footnote",
refresh: "更新",
noActiveFile: "Markdown ファイルが開かれていません",
openMarkdownNote: "Markdown ノートを開くと、その脚注を編集できます。",
readFailed: "ファイルの読み込みに失敗しました: {message}",
noFootnotes: "このノートには脚注定義が見つかりません。",
footnoteCount: "{file} · 脚注 {count} 件",
filteredFootnoteCount: "{file} · {visible}/{total} 件を表示 · {matches} 件一致",
searchPlaceholder: "脚注検索;^番号/名前で移動",
searchTooltip: "脚注本文を検索します。^ に続けて脚注番号または名前を入力すると、その脚注へ移動できます。例: ^42 または ^citation。",
clearSearch: "クリア",
resumeSearch: "検索に戻る",
noSearchResults: "一致する脚注がありません。",
previousMatch: "前の一致",
nextMatch: "次の一致",
searchMatchCount: "{current}/{total}",
multipleReferences: "{count} 箇所参照",
referencePosition: "{current}/{total}",
previousReference: "前の参照",
nextReference: "次の参照",
expandFootnote: "展開",
collapseFootnote: "折りたたむ",
definitionButton: "脚注欄",
definitionTooltip: "文末の脚注定義へ移動",
saved: "保存済み",
saving: "保存中...",
saveError: "エラー: {message}",
characters: "{count}字",
words: "{count}語",
noActiveFileSave: "Markdown ファイルが開かれていません。",
openSourceForReference: "脚注参照へ移動するには、元のノートを開いてください。",
openSourceForDefinition: "脚注定義へ移動するには、元のノートを開いてください。",
noReferenceFound: "[^{id}] の本文参照が見つかりません。",
footnoteNotFound: "脚注 [^{id}] が見つかりません。",
saveConflict: "脚注 [^{id}] はこのカードを開いた後にノート側で変更されました。上書きを避けるため、今回の編集は保存していません。入力した文字は下に残しています:",
saveConflictStatus: "未保存: ノートが変更されました",
saveMissing: "脚注 [^{id}] はノートにもうありません(別の場所で番号が変わったか削除された可能性があります)。そのため今回の編集は保存していません。入力した文字は下に残しています:",
unreferenced: "未参照",
duplicateNumberBadge: "番号使用中",
duplicateNumberHint: "脚注欄でこの定義に新しい番号を付けてください。",
autoTidyPausedDuplicate: "同じ番号の脚注定義があるため、自動整理を一時停止しました。",
commandOpen: "Better Footnote を開く",
ribbonOpen: "Better Footnote を開く",
settingsTitle: "Better Footnote",
countModeName: "脚注のカウント方式",
countModeDesc: "自動ではプラグインの表示言語に従います。英語は単語数、中国語・日本語・韓国語は文字数を数えます。執筆言語が UI と異なる場合は、別の方式を選べます。",
countModeAuto: "自動",
countModeCharacters: "文字数",
countModeWords: "単語数",
renderMarkdownName: "サイドバーで Markdown をレンダリング",
renderMarkdownDesc: "有効にすると、編集していない脚注カードはリンクや太字などを含むレンダリング済みの Markdown で表示されます。リンクをクリックすると開きます。本文をクリックすると埋め込みライブプレビューエディタで編集でき、入力中も書式はレンダリングされたままです。利用できない場合は自動的にソース編集ボックスへ戻ります。",
autoTidyName: "新しい脚注の後に自動整理",
autoTidyDesc: "Tidy Footnotes のインストールと有効化が必要です。このノートのエディタで新しい脚注を挿入すると、Better Footnote が Tidy Footnotes を自動実行します(同期やファイルを直接書き換えるスクリプトで増えた脚注はハイライトのみで、自動整理はしません)。有効にすると Obsidian 標準の脚注フローティング編集欄を閉じるため、脚注は Better Footnote サイドバーで編集してください。",
tidyInstallName: "Tidy Footnotes 連携",
tidyInstallDesc: "自動整理を使う前に、Tidy Footnotes をインストールして有効化してください。",
tidyInstallButton: "プラグインページを開く",
tidyCommandMissing: "Tidy Footnotes コマンドが見つかりません。",
tidyCommandNoEditor: "Tidy Footnotes に使用できる Markdown エディタがありません。",
tidyCommandFailed: "Tidy Footnotes の実行に失敗しました: {message}",
deleteFootnoteMenu: "この脚注を削除",
deleteFootnoteTitle: "脚注 [^{id}] を削除しますか?",
deleteFootnoteWithReferences: "{count} 件の参照マーカーと脚注定義を削除します。",
deleteUnreferencedFootnote: "この脚注には参照マーカーがありません。脚注定義を削除します。",
deleteEmptyFootnote: "この脚注定義は空です。",
deleteCancel: "キャンセル",
deleteConfirm: "脚注を削除",
deleteNeedsEditor: "脚注を削除する前に、元のノートを開いてください。",
switchToEditingView: "先にノートを編集ビューに切り替えてください。",
deleteFailed: "脚注の削除に失敗しました: {message}",
deletedFootnote: "脚注 [^{id}] を削除しました。{shortcut} で取り消せます。",
duplicateFootnoteInserted: "新しい脚注 [^{id}] は既存の脚注と番号が重複しています。挿入したマーカーは既存脚注への参照として扱われ、未参照の重複定義が残っています。取り消して未使用の番号を使うことをおすすめします。",
},
ko: {
title: "Better Footnote",
refresh: "새로고침",
noActiveFile: "열린 Markdown 파일이 없습니다",
openMarkdownNote: "Markdown 노트를 열면 해당 각주를 편집할 수 있습니다.",
readFailed: "파일을 읽지 못했습니다: {message}",
noFootnotes: "이 노트에서 각주 정의를 찾지 못했습니다.",
footnoteCount: "{file} · 각주 {count}개",
filteredFootnoteCount: "{file} · {visible}/{total}개 표시 · {matches}개 일치",
searchPlaceholder: "각주 검색; ^번호/이름 바로 이동",
searchTooltip: "각주 내용을 검색합니다. ^ 뒤에 각주 번호나 이름을 입력하면 해당 각주로 바로 이동합니다. 예: ^42 또는 ^citation.",
clearSearch: "지우기",
resumeSearch: "검색 재개",
noSearchResults: "일치하는 각주가 없습니다.",
previousMatch: "이전 일치",
nextMatch: "다음 일치",
searchMatchCount: "{current}/{total}",
multipleReferences: "{count}개 참조",
referencePosition: "{current}/{total}",
previousReference: "이전 참조",
nextReference: "다음 참조",
expandFootnote: "펼치기",
collapseFootnote: "접기",
definitionButton: "각주 영역",
definitionTooltip: "문서 끝의 각주 정의 위치로 이동",
saved: "저장됨",
saving: "저장 중...",
saveError: "오류: {message}",
characters: "{count}자",
words: "{count}단어",
noActiveFileSave: "열린 Markdown 파일이 없습니다.",
openSourceForReference: "각주 참조로 이동하려면 원본 노트를 여세요.",
openSourceForDefinition: "각주 정의로 이동하려면 원본 노트를 여세요.",
noReferenceFound: "[^{id}]의 본문 참조를 찾지 못했습니다.",
footnoteNotFound: "각주 [^{id}]를 찾지 못했습니다.",
saveConflict: "각주 [^{id}]가 이 카드를 연 뒤 노트에서 변경되었습니다. 덮어쓰기를 막기 위해 이번 편집은 저장하지 않았습니다. 입력한 내용은 아래에 남겨 두었습니다:",
saveConflictStatus: "저장 안 됨: 노트가 변경됨",
saveMissing: "각주 [^{id}]가 노트에 더 이상 없습니다(다른 곳에서 번호가 바뀌었거나 삭제되었을 수 있습니다). 그래서 이번 편집은 저장하지 않았습니다. 입력한 내용은 아래에 남겨 두었습니다:",
unreferenced: "참조 없음",
duplicateNumberBadge: "번호 사용 중",
duplicateNumberHint: "각주 영역에서 이 정의에 새 번호를 붙여 주세요.",
autoTidyPausedDuplicate: "번호가 같은 각주 정의가 있어 자동 정리를 일시 중지했습니다.",
commandOpen: "Better Footnote 열기",
ribbonOpen: "Better Footnote 열기",
settingsTitle: "Better Footnote",
countModeName: "각주 계산 방식",
countModeDesc: "자동은 플러그인 표시 언어를 따릅니다. 영어는 단어 수, 중국어·일본어·한국어는 글자 수를 셉니다. 작성 언어가 UI 언어와 다르면 다른 계산 방식을 선택할 수 있습니다.",
countModeAuto: "자동",
countModeCharacters: "글자 수",
countModeWords: "단어 수",
renderMarkdownName: "사이드바에서 Markdown 렌더링",
renderMarkdownDesc: "켜면 편집 중이 아닌 각주 카드가 링크, 굵게 등 서식을 포함한 렌더링된 Markdown으로 표시됩니다. 링크를 클릭하면 링크가 열립니다. 본문을 클릭하면 내장 라이브 프리뷰 편집기에서 편집할 수 있으며 입력 중에도 서식이 렌더링된 채 유지됩니다. 사용할 수 없는 경우 자동으로 소스 편집 상자로 돌아갑니다.",
autoTidyName: "새 각주 뒤 자동 정리",
autoTidyDesc: "Tidy Footnotes를 먼저 설치하고 활성화해야 합니다. 이 노트의 편집기에서 새 각주를 삽입하면 Better Footnote가 Tidy Footnotes를 자동으로 실행합니다(동기화나 파일을 직접 쓰는 스크립트로 들어온 각주는 강조 표시만 합니다). 이 기능을 켜면 Obsidian 기본 각주 플로팅 편집 창을 닫으므로, 각주는 Better Footnote 사이드바에서 편집하세요.",
tidyInstallName: "Tidy Footnotes 연동",
tidyInstallDesc: "자동 정리를 사용하기 전에 Tidy Footnotes를 설치하고 활성화하세요.",
tidyInstallButton: "플러그인 페이지 열기",
tidyCommandMissing: "Tidy Footnotes 명령을 찾지 못했습니다.",
tidyCommandNoEditor: "Tidy Footnotes에 사용할 Markdown 편집기가 없습니다.",
tidyCommandFailed: "Tidy Footnotes 실행 실패: {message}",
deleteFootnoteMenu: "이 각주 삭제",
deleteFootnoteTitle: "각주 [^{id}]를 삭제할까요?",
deleteFootnoteWithReferences: "참조 표시 {count}개와 각주 정의를 삭제합니다.",
deleteUnreferencedFootnote: "이 각주에는 참조 표시가 없습니다. 각주 정의를 삭제합니다.",
deleteEmptyFootnote: "이 각주 정의는 비어 있습니다.",
deleteCancel: "취소",
deleteConfirm: "각주 삭제",
deleteNeedsEditor: "각주를 삭제하기 전에 원본 노트를 여세요.",
switchToEditingView: "먼저 노트를 편집 보기로 전환하세요.",
deleteFailed: "각주 삭제 실패: {message}",
deletedFootnote: "각주 [^{id}]를 삭제했습니다. {shortcut}로 되돌릴 수 있습니다.",
duplicateFootnoteInserted: "새 각주 [^{id}]가 기존 각주와 번호가 겹칩니다. 삽입한 표시는 기존 각주에 대한 참조로 처리되었고, 참조 없는 중복 정의가 남았습니다. 되돌린 뒤 사용하지 않은 번호를 쓰는 것이 좋습니다.",
},
};
const NUMBER_LOCALES = {
en: "en-US",
zh: "zh-CN",
ja: "ja-JP",
ko: "ko-KR",
};
function t(strings, key, replacements = {}) {
return String(strings[key] ?? I18N.en[key] ?? key).replace(/\{(\w+)}/g, (_match, name) => {
return Object.prototype.hasOwnProperty.call(replacements, name) ? String(replacements[name]) : "";
});
}
function normalizeLanguageTag(rawLanguage) {
if (!rawLanguage) return "en";
const value = String(rawLanguage).replace(/_/g, "-").toLowerCase();
if (value.startsWith("zh")) return "zh";
if (value.startsWith("ja")) return "ja";
if (value.startsWith("ko")) return "ko";
return "en";
}
function readLocalStorageLanguage() {
if (typeof window === "undefined" || !window.localStorage) return "";
try {
return window.localStorage.getItem("language") || "";
} catch (_error) {
return "";
}
}
function getLanguageSignal() {
const obsidianLanguage = readLocalStorageLanguage();
if (obsidianLanguage) return obsidianLanguage;
if (typeof document !== "undefined" && document.documentElement?.lang) {
return document.documentElement.lang;
}
if (typeof window !== "undefined" && typeof window.moment?.locale === "function") {
return window.moment.locale();
}
if (typeof navigator !== "undefined") {
const systemLanguage = navigator.languages?.[0] || navigator.language;
if (systemLanguage) return systemLanguage;
}
return "en";
}
function getUiLanguage() {
return normalizeLanguageTag(getLanguageSignal());
}
function getStrings() {
return I18N[getUiLanguage()] || I18N.en;
}
function formatNumber(value) {
return Number(value).toLocaleString(NUMBER_LOCALES[getUiLanguage()] || NUMBER_LOCALES.en);
}
function getUndoShortcutLabel() {
if (typeof navigator === "undefined") return "Ctrl+Z";
const platformSignal = `${navigator.platform || ""} ${navigator.userAgent || ""}`;
return /Mac|iPhone|iPad|iPod/i.test(platformSignal) ? "Command+Z" : "Ctrl+Z";
}
function normalizeCountMode(mode) {
return COUNT_MODES.has(mode) ? mode : "auto";
}
function resolveCountMode(mode = "auto", language = getUiLanguage()) {
const normalizedMode = normalizeCountMode(mode);
if (normalizedMode !== "auto") return normalizedMode;
return normalizeLanguageTag(language) === "en" ? "words" : "characters";
}
function countCharacters(text) {
return Array.from(String(text ?? "").replace(/\s+/g, "")).length;
}
function countWords(text) {
const matches = String(text ?? "").match(/[\p{L}\p{N}]+(?:['’][\p{L}\p{N}]+)*/gu);
return matches ? matches.length : 0;
}
function countFootnoteText(text, mode = "auto", language = getUiLanguage()) {
return resolveCountMode(mode, language) === "words" ? countWords(text) : countCharacters(text);
}
function formatFootnoteCount(text, mode = "auto", strings = getStrings(), language = getUiLanguage()) {
const resolvedMode = resolveCountMode(mode, language);
const count = countFootnoteText(text, resolvedMode, language);
const key = resolvedMode === "words" ? "words" : "characters";
return t(strings, key, {
count: formatNumber(count),
plural: count === 1 ? "" : "s",
});
}
function formatCharacterCount(value, strings = getStrings()) {
return t(strings, "characters", {
count: formatNumber(value),
plural: Number(value) === 1 ? "" : "s",
});
}
function normalizeForSearch(value) {
return String(value ?? "")
.normalize("NFKC")
.toLocaleLowerCase()
.trim();
}
function getSearchTokens(query) {
return normalizeForSearch(query).split(/\s+/).filter(Boolean);
}
function getExactFootnoteIdSearchQuery(query) {
const normalized = normalizeForSearch(query);
if (!normalized.startsWith("^")) return null;
const id = normalized.slice(1).trim();
return id || null;
}
function isExactFootnoteIdMatch(footnote, exactId) {
return normalizeForSearch(footnote?.id) === exactId;
}
function getFootnoteSearchText(footnote) {
return normalizeForSearch([
footnote.displayNumber,
footnote.index,
footnote.id,
`[^${footnote.id}]`,
footnote.content,
].join(" "));
}
function filterFootnotes(footnotes, query) {
const exactId = getExactFootnoteIdSearchQuery(query);
if (exactId) {
return footnotes.filter((footnote) => isExactFootnoteIdMatch(footnote, exactId));
}
const tokens = getSearchTokens(query);
if (tokens.length === 0) return footnotes;
return footnotes.filter((footnote) => {
const haystack = getFootnoteSearchText(footnote);
return tokens.every((token) => haystack.includes(token));
});
}
function getContentSearchTokens(query) {
const tokens = [];
const seen = new Set();
for (const token of String(query ?? "").trim().split(/\s+/).filter(Boolean)) {
const key = token.toLocaleLowerCase();
if (seen.has(key)) continue;
seen.add(key);
tokens.push(token);
}
return tokens;
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// Matched on the original text, so offsets stay exact even where lowercasing
// would change the length (U+0130 lowercases to two code units).
function findFootnoteContentMatches(footnote, query) {
const tokens = getContentSearchTokens(query);
if (tokens.length === 0) return [];
const content = String(footnote.content || "");
const matches = [];
for (const token of tokens) {
const pattern = new RegExp(escapeRegExp(token), "giu");
let match;
while ((match = pattern.exec(content)) !== null) {
if (match[0].length === 0) {
pattern.lastIndex += 1;
continue;
}
matches.push({
footnoteId: footnote.id,
start: match.index,
end: match.index + match[0].length,
text: match[0],
});
}
}
return matches.sort((left, right) => left.start - right.start || right.end - left.end);
}
function findFootnoteSearchResults(footnotes, query) {
const exactId = getExactFootnoteIdSearchQuery(query);
if (exactId) {
return footnotes
.filter((footnote) => isExactFootnoteIdMatch(footnote, exactId))
.map((footnote) => ({ footnoteId: footnote.id, match: null }));
}
const tokens = getSearchTokens(query);
if (tokens.length === 0) return [];
const results = [];
for (const footnote of filterFootnotes(footnotes, query)) {
const contentMatches = findFootnoteContentMatches(footnote, query);
if (contentMatches.length > 0) {
for (const match of contentMatches) {
results.push({ footnoteId: footnote.id, match });
}
} else {
results.push({ footnoteId: footnote.id, match: null });
}
}
return results;
}
function getFootnoteDisplayOrderValue(footnote) {
if (typeof footnote.firstReferenceStart === "number") return footnote.firstReferenceStart;
return Number.MAX_SAFE_INTEGER;
}
function orderFootnotesByReference(footnotes) {
return Array.from(footnotes || []).sort((left, right) => {
const referenceOrder = getFootnoteDisplayOrderValue(left) - getFootnoteDisplayOrderValue(right);
if (referenceOrder !== 0) return referenceOrder;
const displayOrder = (left.displayNumber || left.index || 0) - (right.displayNumber || right.index || 0);
if (displayOrder !== 0) return displayOrder;
return (left.definitionStart || 0) - (right.definitionStart || 0);
});
}
function countFootnoteIdOccurrences(ids) {
const counts = new Map();
for (const id of ids) {
counts.set(id, (counts.get(id) || 0) + 1);
}
return counts;
}
function detectAddedFootnotes(currentFootnotes, knownFootnoteIds, knownFootnoteSnapshots = null) {
if (!knownFootnoteIds) return [];
const knownCounts = countFootnoteIdOccurrences(Array.from(knownFootnoteIds));
const seenCounts = new Map();
// Only a footnote whose id has disappeared can have been renamed (Tidy):
// a snapshot of an id still present must not vouch for a new neighbour
// inserted right before it at the same offset.
const currentIds = new Set(currentFootnotes.map((footnote) => footnote.id));
const renameCandidates = Array.isArray(knownFootnoteSnapshots)
? knownFootnoteSnapshots.filter((snapshot) => snapshot && !currentIds.has(snapshot.id))
: null;
return currentFootnotes.filter((footnote) => {
const seen = (seenCounts.get(footnote.id) || 0) + 1;
seenCounts.set(footnote.id, seen);
const knownCount = knownCounts.get(footnote.id) || 0;
if (seen <= knownCount) return false;
if (knownCount === 0 && isKnownFootnoteBySnapshot(footnote, renameCandidates)) return false;
return true;
});
}
function choosePrimaryAddedFootnote(addedFootnotes) {
if (addedFootnotes.length === 0) return null;
const emptyAddedFootnote = addedFootnotes.find((footnote) => !String(footnote.content || "").trim());
return emptyAddedFootnote || addedFootnotes[0];
}
function getFootnoteFingerprint(footnote) {
return normalizeForSearch(normalizeLineEndings(footnote?.content || "").replace(/\s+/g, " "));
}
function createFootnoteSnapshot(footnote) {
if (!footnote) return null;
return {
id: footnote.id,
displayNumber: footnote.displayNumber || footnote.index || null,
contentFingerprint: getFootnoteFingerprint(footnote),
definitionStart: footnote.definitionStart,
firstReferenceStart: footnote.firstReferenceStart,
};
}
function deletedFootnoteRecordMatchesFootnote(record, footnote) {
if (!record || !footnote) return false;
if (record.id !== String(footnote.id)) return false;
const deletedFingerprint = record.snapshot?.contentFingerprint || "";
const currentFingerprint = getFootnoteFingerprint(footnote);
if (deletedFingerprint !== currentFingerprint) return false;
if (deletedFingerprint) return true;
// Empty footnotes carry no fingerprint, and Obsidian reuses the freed
// number for the next insert, so a brand-new empty footnote would look
// like an undo. Treat it as restored only when it reappears at the same
// reference position (or, if unreferenced, the same definition position).
const snapshot = record.snapshot || {};
const sameReference = typeof snapshot.firstReferenceStart === "number" || typeof footnote.firstReferenceStart === "number"
? snapshot.firstReferenceStart === footnote.firstReferenceStart
: true;
return sameReference && snapshot.definitionStart === footnote.definitionStart;
}
function normalizeReferenceIndex(footnote, index = 0) {
const count = Math.max(0, Number(footnote?.references?.length || footnote?.referenceCount || 0));
if (count <= 0) return 0;
const numericIndex = Number.isFinite(Number(index)) ? Number(index) : 0;
return Math.max(0, Math.min(count - 1, Math.trunc(numericIndex)));
}
function referenceIndexForFootnoteReference(footnote, reference) {
if (!footnote || !reference || !Array.isArray(footnote.references)) return 0;
const index = footnote.references.findIndex((item) => {
return item.start === reference.start && item.end === reference.end;
});
return index >= 0 ? index : 0;
}
function isKnownFootnoteBySnapshot(footnote, snapshots) {
if (!Array.isArray(snapshots) || snapshots.length === 0) return false;
if (typeof footnote.firstReferenceStart === "number") {
const referenceMatch = snapshots.some((snapshot) => snapshot.firstReferenceStart === footnote.firstReferenceStart);
if (referenceMatch) return true;
}
const fingerprint = getFootnoteFingerprint(footnote);
if (fingerprint) {
return snapshots.some((snapshot) => snapshot.contentFingerprint === fingerprint);
}
return false;
}
function closestFootnote(candidates, snapshot) {
if (candidates.length === 0) return null;
if (candidates.length === 1) return candidates[0];
return candidates.reduce((best, footnote) => {
const displayDistance = Number.isFinite(snapshot?.displayNumber)
? Math.abs((footnote.displayNumber || footnote.index || 0) - snapshot.displayNumber)
: 0;
const referenceDistance =
typeof snapshot?.firstReferenceStart === "number" && typeof footnote.firstReferenceStart === "number"
? Math.abs(footnote.firstReferenceStart - snapshot.firstReferenceStart)
: 0;
const definitionDistance =
typeof snapshot?.definitionStart === "number"
? Math.abs(footnote.definitionStart - snapshot.definitionStart)
: 0;
const score = displayDistance * 1000000 + referenceDistance + definitionDistance;
if (!best || score < best.score) {
return { footnote, score };
}
return best;
}, null)?.footnote || candidates[0];
}
function resolveFootnoteFromSnapshot(footnotes, snapshot, options = {}) {
if (!snapshot) return null;
const allowDisplayFallback = options.allowDisplayFallback !== false;
if (typeof snapshot.firstReferenceStart === "number") {
const exactReferenceMatch = footnotes.find((footnote) => footnote.firstReferenceStart === snapshot.firstReferenceStart);
if (exactReferenceMatch) return exactReferenceMatch;
const nearbyReferenceMatches = footnotes.filter((footnote) => {
return typeof footnote.firstReferenceStart === "number"
&& Math.abs(footnote.firstReferenceStart - snapshot.firstReferenceStart) <= 8;
});
const nearbyReferenceMatch = closestFootnote(nearbyReferenceMatches, snapshot);
if (nearbyReferenceMatch) return nearbyReferenceMatch;
}
if (snapshot.contentFingerprint) {
const contentMatches = footnotes.filter((footnote) => getFootnoteFingerprint(footnote) === snapshot.contentFingerprint);
const matched = closestFootnote(contentMatches, snapshot);
if (matched) return matched;
}
if (allowDisplayFallback && Number.isFinite(snapshot.displayNumber)) {
const displayMatch = footnotes.find((footnote) => (footnote.displayNumber || footnote.index) === snapshot.displayNumber);
if (displayMatch) return displayMatch;
}
return null;
}
function resolveActiveFootnoteId(footnotes, savedState = {}, fallbackId = null) {
const activeId = savedState.activeId || fallbackId;
const snapshot = savedState.activeSnapshot || null;
const strongSnapshotMatch = resolveFootnoteFromSnapshot(footnotes, snapshot, { allowDisplayFallback: false });
if (strongSnapshotMatch) {
return strongSnapshotMatch.id;
}
if (activeId && footnotes.some((footnote) => footnote.id === activeId)) {
return activeId;
}
const snapshotMatch = resolveFootnoteFromSnapshot(footnotes, snapshot);
if (snapshotMatch) {
return snapshotMatch.id;
}
return activeId || null;
}
function normalizeLineEndings(text) {
return String(text ?? "").replace(/\r\n?/g, "\n");
}
function getLineStarts(text) {
const starts = [0];
for (let index = 0; index < text.length; index += 1) {
if (text.charCodeAt(index) === 10) {
starts.push(index + 1);
}
}
return starts;
}
function lineIndexFromOffset(lineStarts, offset) {
let low = 0;
let high = lineStarts.length - 1;
while (low <= high) {
const mid = Math.floor((low + high) / 2);
const next = mid + 1 < lineStarts.length ? lineStarts[mid + 1] : Infinity;
if (offset < lineStarts[mid]) {
high = mid - 1;
} else if (offset >= next) {
low = mid + 1;
} else {
return mid;
}
}
return Math.max(0, Math.min(lineStarts.length - 1, low));
}
function positionFromOffset(text, offset) {
const safeOffset = Math.max(0, Math.min(text.length, offset));
const starts = getLineStarts(text);
const line = lineIndexFromOffset(starts, safeOffset);
return {
line,
ch: safeOffset - starts[line],
};
}
function isFootnoteDefinitionLine(line) {
return /^( {0,3})\[\^([^\]\n]+)]:[ \t]?/.test(line);
}
function isContinuationLine(line) {
return line === "" || /^(?: {2,}|\t)/.test(line);
}
function isIndentedContinuationLine(line) {
return /^(?: {2,}|\t)/.test(line);
}
function blankLineContinuesFootnote(lines, blankLineIndex) {
for (let index = blankLineIndex + 1; index < lines.length; index += 1) {
const line = lines[index];
if (line === "") continue;
if (isFootnoteDefinitionLine(line)) return false;
return isIndentedContinuationLine(line);
}
return false;
}
function unindentContinuation(line) {
if (line.startsWith("\t")) return line.slice(1);
const spaceIndent = line.match(/^ {2,4}/);
if (spaceIndent) return line.slice(spaceIndent[0].length);
return line;
}
// Spans where footnote syntax is only text: front matter, fenced code,
// inline code, Obsidian and HTML comments, math. Only closed spans count, so
// typing an opening fence or %% never makes the footnotes below it vanish.
function getMarkdownExclusionZones(text, lineStarts = getLineStarts(text)) {
const blocks = [];
const lines = text.split("\n");
let firstLine = 0;
if (lines[0] === "---") {
for (let index = 1; index < lines.length; index += 1) {
if (lines[index] === "---" || lines[index] === "...") {
blocks.push({ start: 0, end: lineStarts[index] + lines[index].length });
firstLine = index + 1;
break;
}
}
}
let fence = null;
for (let index = firstLine; index < lines.length; index += 1) {
const line = lines[index];
if (!fence) {
const open = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);
if (open && !(open[1][0] === "`" && open[2].includes("`"))) {
fence = { char: open[1][0], length: open[1].length, start: lineStarts[index] };
}
continue;
}
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
if (close && close[1][0] === fence.char && close[1].length >= fence.length) {
blocks.push({ start: fence.start, end: lineStarts[index] + line.length });
fence = null;
}
}
const zones = blocks.slice();
const tokenPattern = /\\|`+|%%|<!--|\$\$/g;
const blankLinePattern = /\n[ \t]*\n/g;
let position = 0;
let blockIndex = 0;
while (position < text.length) {
while (blockIndex < blocks.length && blocks[blockIndex].end <= position) blockIndex += 1;
const block = blocks[blockIndex];
if (block && position >= block.start) {
position = block.end;
continue;
}
const limit = block ? block.start : text.length;
tokenPattern.lastIndex = position;
const token = tokenPattern.exec(text);
if (!token || token.index >= limit) {
position = limit;
continue;
}
const at = token.index;
const value = token[0];
if (value === "\\") {
position = at + 2;
continue;
}
let end = -1;
if (value[0] === "`") {
blankLinePattern.lastIndex = at;
const blank = blankLinePattern.exec(text);
const paragraphEnd = Math.min(limit, blank ? blank.index : text.length);
const runPattern = /`+/g;
runPattern.lastIndex = at + value.length;
let run;
while ((run = runPattern.exec(text)) !== null && run.index < paragraphEnd) {
if (run[0].length === value.length) {
end = run.index + run[0].length;
break;
}
}
} else {
const closer = value === "<!--" ? "-->" : value;
const found = text.indexOf(closer, at + value.length);
if (found !== -1) end = found + closer.length;
}
if (end === -1) {
position = at + value.length;
continue;
}
zones.push({ start: at, end });
position = end;
}
zones.sort((left, right) => left.start - right.start);
const merged = [];
for (const zone of zones) {
const last = merged[merged.length - 1];
if (last && zone.start <= last.end) {
last.end = Math.max(last.end, zone.end);
} else {
merged.push({ start: zone.start, end: zone.end });
}
}
return merged;
}
function isOffsetInZones(zones, offset) {
let low = 0;
let high = zones.length - 1;
while (low <= high) {
const mid = (low + high) >> 1;
if (offset < zones[mid].start) {
high = mid - 1;
} else if (offset >= zones[mid].end) {
low = mid + 1;
} else {
return true;
}
}
return false;
}
function isEscapedAt(text, offset) {
let count = 0;
for (let index = offset - 1; index >= 0 && text.charCodeAt(index) === 92; index -= 1) count += 1;
return count % 2 === 1;
}
function buildFootnoteBlock(footnote, content) {
const lines = normalizeLineEndings(content).split("\n");
const firstLine = lines.shift() ?? "";
const continuationIndent = `${footnote.indent || ""}${FOOTNOTE_CONTINUATION_INDENT}`;
const suffix = lines.map((line) => `\n${continuationIndent}${line}`).join("");
return `${footnote.indent || ""}[^${footnote.id}]: ${firstLine}${suffix}`;
}
function parseFootnotes(rawText) {
const text = normalizeLineEndings(rawText);
const lines = text.split("\n");
const lineStarts = getLineStarts(text);
const zones = getMarkdownExclusionZones(text, lineStarts);
const definitions = [];
const references = [];
const definitionById = new Map();
const occurrenceById = new Map();
const hiddenDefinitionIds = [];
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const line = lines[lineIndex];
const match = line.match(/^( {0,3})\[\^([^\]\n]+)]:[ \t]?(.*)$/);
if (!match) continue;
if (isOffsetInZones(zones, lineStarts[lineIndex])) {
hiddenDefinitionIds.push(match[2]);
continue;
}
const indent = match[1] || "";
const id = match[2];
const firstContent = match[3] || "";
const contentLines = [firstContent];
const definitionStart = lineStarts[lineIndex];
const contentStart = definitionStart + line.length - firstContent.length;
let endLineIndex = lineIndex;
for (let nextLineIndex = lineIndex + 1; nextLineIndex < lines.length; nextLineIndex += 1) {
const nextLine = lines[nextLineIndex];
if (isFootnoteDefinitionLine(nextLine)) break;
if (nextLine === "" && !blankLineContinuesFootnote(lines, nextLineIndex)) break;
if (!isContinuationLine(nextLine)) break;
contentLines.push(unindentContinuation(nextLine));
endLineIndex = nextLineIndex;
}
const definitionEnd = lineStarts[endLineIndex] + lines[endLineIndex].length;
const occurrence = occurrenceById.get(id) || 0;
occurrenceById.set(id, occurrence + 1);
const footnote = {
id,
indent,
occurrence,
index: definitions.length + 1,
line: lineIndex + 1,
endLine: endLineIndex + 1,
definitionStart,
definitionEnd,
contentStart,
content: contentLines.join("\n"),
references: [],
referenceCount: 0,
firstReferenceStart: null,
};
definitions.push(footnote);
if (!definitionById.has(id)) {
definitionById.set(id, footnote);
}
lineIndex = endLineIndex;
}
const referencePattern = /\[\^([^\]\n]+)]/g;
let referenceMatch;
while ((referenceMatch = referencePattern.exec(text)) !== null) {
const start = referenceMatch.index;
if (isOffsetInZones(zones, start) || isEscapedAt(text, start)) continue;
const end = start + referenceMatch[0].length;
const id = referenceMatch[1];
const lineIndex = lineIndexFromOffset(lineStarts, start);
const beforeOnLine = text.slice(lineStarts[lineIndex], start);
const after = text.slice(end, end + 1);
if (/^ {0,3}$/.test(beforeOnLine) && after === ":") {
continue;
}