@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/dist/index.js CHANGED
@@ -520,6 +520,557 @@ function joinLines(lines, start, end) {
520
520
  return lines.slice(start, end + 1).join("\n");
521
521
  }
522
522
 
523
- export { IncremarkParser, calculateLineOffset, createIncremarkParser, createInitialContext, detectContainer, detectContainerEnd, detectFenceEnd, detectFenceStart, generateId, isBlockBoundary, isBlockquoteStart, isEmptyLine, isHeading, isHtmlBlock, isListItemStart, isTableDelimiter, isThematicBreak, joinLines, resetIdCounter, splitLines, updateContext };
523
+ // src/transformer/utils.ts
524
+ function countChars(node) {
525
+ let count = 0;
526
+ function traverse(n) {
527
+ if (n.value && typeof n.value === "string") {
528
+ count += n.value.length;
529
+ return;
530
+ }
531
+ if (n.children && Array.isArray(n.children)) {
532
+ for (const child of n.children) {
533
+ traverse(child);
534
+ }
535
+ }
536
+ }
537
+ traverse(node);
538
+ return count;
539
+ }
540
+ function sliceAst(node, maxChars, accumulatedChunks) {
541
+ if (maxChars <= 0) return null;
542
+ let remaining = maxChars;
543
+ let charIndex = 0;
544
+ const chunkRanges = [];
545
+ if (accumulatedChunks && accumulatedChunks.chunks.length > 0) {
546
+ let chunkStart = accumulatedChunks.stableChars;
547
+ for (const chunk of accumulatedChunks.chunks) {
548
+ chunkRanges.push({
549
+ start: chunkStart,
550
+ end: chunkStart + chunk.text.length,
551
+ chunk
552
+ });
553
+ chunkStart += chunk.text.length;
554
+ }
555
+ }
556
+ function process(n) {
557
+ if (remaining <= 0) return null;
558
+ if (n.value && typeof n.value === "string") {
559
+ const take = Math.min(n.value.length, remaining);
560
+ remaining -= take;
561
+ if (take === 0) return null;
562
+ const slicedValue = n.value.slice(0, take);
563
+ const nodeStart = charIndex;
564
+ const nodeEnd = charIndex + take;
565
+ charIndex += take;
566
+ const result = {
567
+ ...n,
568
+ value: slicedValue
569
+ };
570
+ if (chunkRanges.length > 0 && accumulatedChunks) {
571
+ const nodeChunks = [];
572
+ let firstChunkLocalStart = take;
573
+ for (const range of chunkRanges) {
574
+ const overlapStart = Math.max(range.start, nodeStart);
575
+ const overlapEnd = Math.min(range.end, nodeEnd);
576
+ if (overlapStart < overlapEnd) {
577
+ const localStart = overlapStart - nodeStart;
578
+ const localEnd = overlapEnd - nodeStart;
579
+ const chunkText = slicedValue.slice(localStart, localEnd);
580
+ if (chunkText.length > 0) {
581
+ if (nodeChunks.length === 0) {
582
+ firstChunkLocalStart = localStart;
583
+ }
584
+ nodeChunks.push({
585
+ text: chunkText,
586
+ createdAt: range.chunk.createdAt
587
+ });
588
+ }
589
+ }
590
+ }
591
+ if (nodeChunks.length > 0) {
592
+ result.stableLength = firstChunkLocalStart;
593
+ result.chunks = nodeChunks;
594
+ }
595
+ }
596
+ return result;
597
+ }
598
+ if (n.children && Array.isArray(n.children)) {
599
+ const newChildren = [];
600
+ for (const child of n.children) {
601
+ if (remaining <= 0) break;
602
+ const processed = process(child);
603
+ if (processed) {
604
+ newChildren.push(processed);
605
+ }
606
+ }
607
+ if (newChildren.length === 0) {
608
+ return null;
609
+ }
610
+ return { ...n, children: newChildren };
611
+ }
612
+ remaining -= 1;
613
+ charIndex += 1;
614
+ return { ...n };
615
+ }
616
+ return process(node);
617
+ }
618
+ function cloneNode(node) {
619
+ return JSON.parse(JSON.stringify(node));
620
+ }
621
+
622
+ // src/transformer/BlockTransformer.ts
623
+ var BlockTransformer = class {
624
+ state;
625
+ options;
626
+ rafId = null;
627
+ lastTickTime = 0;
628
+ isRunning = false;
629
+ isPaused = false;
630
+ chunks = [];
631
+ // 累积的 chunks(用于 fade-in 动画)
632
+ visibilityHandler = null;
633
+ constructor(options = {}) {
634
+ this.options = {
635
+ charsPerTick: options.charsPerTick ?? 1,
636
+ tickInterval: options.tickInterval ?? 20,
637
+ effect: options.effect ?? "none",
638
+ plugins: options.plugins ?? [],
639
+ onChange: options.onChange ?? (() => {
640
+ }),
641
+ pauseOnHidden: options.pauseOnHidden ?? true
642
+ };
643
+ this.state = {
644
+ completedBlocks: [],
645
+ currentBlock: null,
646
+ currentProgress: 0,
647
+ pendingBlocks: []
648
+ };
649
+ if (this.options.pauseOnHidden && typeof document !== "undefined") {
650
+ this.setupVisibilityHandler();
651
+ }
652
+ }
653
+ /**
654
+ * 推入新的 blocks
655
+ * 会自动过滤已存在的 blocks
656
+ */
657
+ push(blocks) {
658
+ const existingIds = this.getAllBlockIds();
659
+ const newBlocks = blocks.filter((b) => !existingIds.has(b.id));
660
+ if (newBlocks.length > 0) {
661
+ this.state.pendingBlocks.push(...newBlocks);
662
+ this.startIfNeeded();
663
+ }
664
+ if (this.state.currentBlock) {
665
+ const updated = blocks.find((b) => b.id === this.state.currentBlock.id);
666
+ if (updated && updated.node !== this.state.currentBlock.node) {
667
+ const oldTotal = this.countChars(this.state.currentBlock.node);
668
+ const newTotal = this.countChars(updated.node);
669
+ if (newTotal < oldTotal || newTotal < this.state.currentProgress) {
670
+ this.state.currentProgress = Math.min(this.state.currentProgress, newTotal);
671
+ }
672
+ this.state.currentBlock = updated;
673
+ if (!this.rafId && !this.isPaused) {
674
+ if (this.state.currentProgress < newTotal) {
675
+ this.startIfNeeded();
676
+ }
677
+ }
678
+ }
679
+ }
680
+ }
681
+ /**
682
+ * 更新指定 block(用于 pending block 内容增加时)
683
+ */
684
+ update(block) {
685
+ if (this.state.currentBlock?.id === block.id) {
686
+ const oldTotal = this.countChars(this.state.currentBlock.node);
687
+ const newTotal = this.countChars(block.node);
688
+ this.state.currentBlock = block;
689
+ if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
690
+ this.startIfNeeded();
691
+ }
692
+ }
693
+ }
694
+ /**
695
+ * 跳过所有动画,直接显示全部内容
696
+ */
697
+ skip() {
698
+ this.stop();
699
+ const allBlocks = [
700
+ ...this.state.completedBlocks,
701
+ ...this.state.currentBlock ? [this.state.currentBlock] : [],
702
+ ...this.state.pendingBlocks
703
+ ];
704
+ this.state = {
705
+ completedBlocks: allBlocks,
706
+ currentBlock: null,
707
+ currentProgress: 0,
708
+ pendingBlocks: []
709
+ };
710
+ this.chunks = [];
711
+ this.emit();
712
+ }
713
+ /**
714
+ * 重置状态
715
+ */
716
+ reset() {
717
+ this.stop();
718
+ this.state = {
719
+ completedBlocks: [],
720
+ currentBlock: null,
721
+ currentProgress: 0,
722
+ pendingBlocks: []
723
+ };
724
+ this.chunks = [];
725
+ this.emit();
726
+ }
727
+ /**
728
+ * 暂停动画
729
+ */
730
+ pause() {
731
+ this.isPaused = true;
732
+ this.cancelRaf();
733
+ }
734
+ /**
735
+ * 恢复动画
736
+ */
737
+ resume() {
738
+ if (this.isPaused) {
739
+ this.isPaused = false;
740
+ this.startIfNeeded();
741
+ }
742
+ }
743
+ /**
744
+ * 获取用于渲染的 display blocks
745
+ */
746
+ getDisplayBlocks() {
747
+ const result = [];
748
+ for (const block of this.state.completedBlocks) {
749
+ result.push({
750
+ ...block,
751
+ displayNode: block.node,
752
+ progress: 1,
753
+ isDisplayComplete: true
754
+ });
755
+ }
756
+ if (this.state.currentBlock) {
757
+ const total = this.countChars(this.state.currentBlock.node);
758
+ const accumulatedChunks = this.options.effect === "fade-in" && this.chunks.length > 0 ? { stableChars: 0, chunks: this.chunks } : void 0;
759
+ const displayNode = this.sliceNode(
760
+ this.state.currentBlock.node,
761
+ this.state.currentProgress,
762
+ accumulatedChunks
763
+ );
764
+ result.push({
765
+ ...this.state.currentBlock,
766
+ displayNode: displayNode || { type: "paragraph", children: [] },
767
+ progress: total > 0 ? this.state.currentProgress / total : 1,
768
+ isDisplayComplete: false
769
+ });
770
+ }
771
+ return result;
772
+ }
773
+ /**
774
+ * 是否正在处理中
775
+ */
776
+ isProcessing() {
777
+ return this.isRunning || this.state.currentBlock !== null || this.state.pendingBlocks.length > 0;
778
+ }
779
+ /**
780
+ * 是否已暂停
781
+ */
782
+ isPausedState() {
783
+ return this.isPaused;
784
+ }
785
+ /**
786
+ * 获取内部状态(用于调试)
787
+ */
788
+ getState() {
789
+ return { ...this.state };
790
+ }
791
+ /**
792
+ * 动态更新配置
793
+ */
794
+ setOptions(options) {
795
+ if (options.charsPerTick !== void 0) {
796
+ this.options.charsPerTick = options.charsPerTick;
797
+ }
798
+ if (options.tickInterval !== void 0) {
799
+ this.options.tickInterval = options.tickInterval;
800
+ }
801
+ if (options.effect !== void 0) {
802
+ this.options.effect = options.effect;
803
+ }
804
+ if (options.pauseOnHidden !== void 0) {
805
+ this.options.pauseOnHidden = options.pauseOnHidden;
806
+ if (options.pauseOnHidden && typeof document !== "undefined") {
807
+ this.setupVisibilityHandler();
808
+ } else {
809
+ this.removeVisibilityHandler();
810
+ }
811
+ }
812
+ }
813
+ /**
814
+ * 获取当前配置
815
+ */
816
+ getOptions() {
817
+ return {
818
+ charsPerTick: this.options.charsPerTick,
819
+ tickInterval: this.options.tickInterval,
820
+ effect: this.options.effect
821
+ };
822
+ }
823
+ /**
824
+ * 获取当前动画效果
825
+ */
826
+ getEffect() {
827
+ return this.options.effect;
828
+ }
829
+ /**
830
+ * 销毁,清理资源
831
+ */
832
+ destroy() {
833
+ this.stop();
834
+ this.removeVisibilityHandler();
835
+ }
836
+ // ============ 私有方法 ============
837
+ getAllBlockIds() {
838
+ return new Set([
839
+ ...this.state.completedBlocks.map((b) => b.id),
840
+ this.state.currentBlock?.id,
841
+ ...this.state.pendingBlocks.map((b) => b.id)
842
+ ].filter((id) => id !== void 0));
843
+ }
844
+ setupVisibilityHandler() {
845
+ if (this.visibilityHandler) return;
846
+ this.visibilityHandler = () => {
847
+ if (document.hidden) {
848
+ this.pause();
849
+ } else {
850
+ this.resume();
851
+ }
852
+ };
853
+ document.addEventListener("visibilitychange", this.visibilityHandler);
854
+ }
855
+ removeVisibilityHandler() {
856
+ if (this.visibilityHandler) {
857
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
858
+ this.visibilityHandler = null;
859
+ }
860
+ }
861
+ startIfNeeded() {
862
+ if (this.rafId || this.isPaused) return;
863
+ if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
864
+ this.state.currentBlock = this.state.pendingBlocks.shift();
865
+ this.state.currentProgress = 0;
866
+ }
867
+ if (this.state.currentBlock) {
868
+ this.isRunning = true;
869
+ this.lastTickTime = 0;
870
+ this.scheduleNextFrame();
871
+ }
872
+ }
873
+ scheduleNextFrame() {
874
+ this.rafId = requestAnimationFrame((time) => this.animationFrame(time));
875
+ }
876
+ animationFrame(time) {
877
+ this.rafId = null;
878
+ if (this.lastTickTime === 0) {
879
+ this.lastTickTime = time;
880
+ }
881
+ const elapsed = time - this.lastTickTime;
882
+ if (elapsed >= this.options.tickInterval) {
883
+ this.lastTickTime = time;
884
+ this.tick();
885
+ }
886
+ if (this.isRunning && !this.isPaused) {
887
+ this.scheduleNextFrame();
888
+ }
889
+ }
890
+ tick() {
891
+ const block = this.state.currentBlock;
892
+ if (!block) {
893
+ this.processNext();
894
+ return;
895
+ }
896
+ const total = this.countChars(block.node);
897
+ const step = this.getStep();
898
+ const prevProgress = this.state.currentProgress;
899
+ this.state.currentProgress = Math.min(prevProgress + step, total);
900
+ if (this.options.effect === "fade-in" && this.state.currentProgress > prevProgress) {
901
+ const newText = this.extractText(block.node, prevProgress, this.state.currentProgress);
902
+ if (newText.length > 0) {
903
+ this.chunks.push({
904
+ text: newText,
905
+ createdAt: Date.now()
906
+ });
907
+ }
908
+ }
909
+ this.emit();
910
+ if (this.state.currentProgress >= total) {
911
+ this.notifyComplete(block.node);
912
+ this.state.completedBlocks.push(block);
913
+ this.state.currentBlock = null;
914
+ this.state.currentProgress = 0;
915
+ this.chunks = [];
916
+ this.processNext();
917
+ }
918
+ }
919
+ /**
920
+ * 从 AST 节点中提取指定范围的文本
921
+ */
922
+ extractText(node, start, end) {
923
+ let result = "";
924
+ let charIndex = 0;
925
+ function traverse(n) {
926
+ if (charIndex >= end) return false;
927
+ if (n.value && typeof n.value === "string") {
928
+ const nodeStart = charIndex;
929
+ const nodeEnd = charIndex + n.value.length;
930
+ charIndex = nodeEnd;
931
+ const overlapStart = Math.max(start, nodeStart);
932
+ const overlapEnd = Math.min(end, nodeEnd);
933
+ if (overlapStart < overlapEnd) {
934
+ result += n.value.slice(overlapStart - nodeStart, overlapEnd - nodeStart);
935
+ }
936
+ return charIndex < end;
937
+ }
938
+ if (n.children && Array.isArray(n.children)) {
939
+ for (const child of n.children) {
940
+ if (!traverse(child)) return false;
941
+ }
942
+ }
943
+ return true;
944
+ }
945
+ traverse(node);
946
+ return result;
947
+ }
948
+ getStep() {
949
+ const { charsPerTick } = this.options;
950
+ if (typeof charsPerTick === "number") {
951
+ return charsPerTick;
952
+ }
953
+ const [min, max] = charsPerTick;
954
+ return Math.floor(Math.random() * (max - min + 1)) + min;
955
+ }
956
+ processNext() {
957
+ if (this.state.pendingBlocks.length > 0) {
958
+ this.state.currentBlock = this.state.pendingBlocks.shift();
959
+ this.state.currentProgress = 0;
960
+ this.chunks = [];
961
+ this.emit();
962
+ } else {
963
+ this.isRunning = false;
964
+ this.cancelRaf();
965
+ this.emit();
966
+ }
967
+ }
968
+ cancelRaf() {
969
+ if (this.rafId) {
970
+ cancelAnimationFrame(this.rafId);
971
+ this.rafId = null;
972
+ }
973
+ }
974
+ stop() {
975
+ this.cancelRaf();
976
+ this.isRunning = false;
977
+ this.isPaused = false;
978
+ }
979
+ emit() {
980
+ this.options.onChange(this.getDisplayBlocks());
981
+ }
982
+ // ============ 插件调用 ============
983
+ countChars(node) {
984
+ for (const plugin of this.options.plugins) {
985
+ if (plugin.match?.(node) && plugin.countChars) {
986
+ const result = plugin.countChars(node);
987
+ if (result !== void 0) return result;
988
+ }
989
+ }
990
+ return countChars(node);
991
+ }
992
+ sliceNode(node, chars, accumulatedChunks) {
993
+ for (const plugin of this.options.plugins) {
994
+ if (plugin.match?.(node) && plugin.sliceNode) {
995
+ const total = this.countChars(node);
996
+ const result = plugin.sliceNode(node, chars, total);
997
+ if (result !== null) return result;
998
+ }
999
+ }
1000
+ return sliceAst(node, chars, accumulatedChunks);
1001
+ }
1002
+ notifyComplete(node) {
1003
+ for (const plugin of this.options.plugins) {
1004
+ if (plugin.match?.(node) && plugin.onComplete) {
1005
+ plugin.onComplete(node);
1006
+ }
1007
+ }
1008
+ }
1009
+ };
1010
+ function createBlockTransformer(options) {
1011
+ return new BlockTransformer(options);
1012
+ }
1013
+
1014
+ // src/transformer/plugins.ts
1015
+ var codeBlockPlugin = {
1016
+ name: "code-block",
1017
+ match: (node) => node.type === "code",
1018
+ countChars: () => 1,
1019
+ // 算作 1 个字符,整体出现
1020
+ sliceNode: (node, displayedChars, totalChars) => {
1021
+ return displayedChars >= totalChars ? node : null;
1022
+ }
1023
+ };
1024
+ var mermaidPlugin = {
1025
+ name: "mermaid",
1026
+ match: (node) => {
1027
+ if (node.type !== "code") return false;
1028
+ const codeNode = node;
1029
+ return codeNode.lang === "mermaid";
1030
+ },
1031
+ countChars: () => 1,
1032
+ sliceNode: (node, displayedChars) => displayedChars > 0 ? node : null
1033
+ };
1034
+ var imagePlugin = {
1035
+ name: "image",
1036
+ match: (node) => node.type === "image",
1037
+ countChars: () => 0
1038
+ // 0 字符,立即显示
1039
+ };
1040
+ var mathPlugin = {
1041
+ name: "math",
1042
+ match: (node) => {
1043
+ const type = node.type;
1044
+ return type === "math" || type === "inlineMath";
1045
+ },
1046
+ countChars: () => 1,
1047
+ sliceNode: (node, displayedChars) => displayedChars > 0 ? node : null
1048
+ };
1049
+ var thematicBreakPlugin = {
1050
+ name: "thematic-break",
1051
+ match: (node) => node.type === "thematicBreak",
1052
+ countChars: () => 0
1053
+ };
1054
+ var defaultPlugins = [
1055
+ imagePlugin,
1056
+ thematicBreakPlugin
1057
+ ];
1058
+ var allPlugins = [
1059
+ mermaidPlugin,
1060
+ // mermaid 优先于普通 code block
1061
+ codeBlockPlugin,
1062
+ imagePlugin,
1063
+ mathPlugin,
1064
+ thematicBreakPlugin
1065
+ ];
1066
+ function createPlugin(name, matcher, options = {}) {
1067
+ return {
1068
+ name,
1069
+ match: matcher,
1070
+ ...options
1071
+ };
1072
+ }
1073
+
1074
+ export { BlockTransformer, IncremarkParser, allPlugins, calculateLineOffset, cloneNode, codeBlockPlugin, countChars, createBlockTransformer, createIncremarkParser, createInitialContext, createPlugin, defaultPlugins, detectContainer, detectContainerEnd, detectFenceEnd, detectFenceStart, generateId, imagePlugin, isBlockBoundary, isBlockquoteStart, isEmptyLine, isHeading, isHtmlBlock, isListItemStart, isTableDelimiter, isThematicBreak, joinLines, mathPlugin, mermaidPlugin, resetIdCounter, sliceAst, splitLines, thematicBreakPlugin, updateContext };
524
1075
  //# sourceMappingURL=index.js.map
525
1076
  //# sourceMappingURL=index.js.map