@incremark/core 0.0.4 → 0.0.5

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,455 @@ 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) {
541
+ if (maxChars <= 0) return null;
542
+ let remaining = maxChars;
543
+ function process(n) {
544
+ if (remaining <= 0) return null;
545
+ if (n.value && typeof n.value === "string") {
546
+ const take = Math.min(n.value.length, remaining);
547
+ remaining -= take;
548
+ if (take === 0) return null;
549
+ return { ...n, value: n.value.slice(0, take) };
550
+ }
551
+ if (n.children && Array.isArray(n.children)) {
552
+ const newChildren = [];
553
+ for (const child of n.children) {
554
+ if (remaining <= 0) break;
555
+ const processed = process(child);
556
+ if (processed) {
557
+ newChildren.push(processed);
558
+ }
559
+ }
560
+ if (newChildren.length === 0) {
561
+ return null;
562
+ }
563
+ return { ...n, children: newChildren };
564
+ }
565
+ remaining -= 1;
566
+ return { ...n };
567
+ }
568
+ return process(node);
569
+ }
570
+ function cloneNode(node) {
571
+ return JSON.parse(JSON.stringify(node));
572
+ }
573
+
574
+ // src/transformer/BlockTransformer.ts
575
+ var BlockTransformer = class {
576
+ state;
577
+ options;
578
+ rafId = null;
579
+ lastTickTime = 0;
580
+ isRunning = false;
581
+ isPaused = false;
582
+ visibilityHandler = null;
583
+ constructor(options = {}) {
584
+ this.options = {
585
+ charsPerTick: options.charsPerTick ?? 1,
586
+ tickInterval: options.tickInterval ?? 20,
587
+ effect: options.effect ?? "none",
588
+ plugins: options.plugins ?? [],
589
+ onChange: options.onChange ?? (() => {
590
+ }),
591
+ pauseOnHidden: options.pauseOnHidden ?? true
592
+ };
593
+ this.state = {
594
+ completedBlocks: [],
595
+ currentBlock: null,
596
+ currentProgress: 0,
597
+ pendingBlocks: []
598
+ };
599
+ if (this.options.pauseOnHidden && typeof document !== "undefined") {
600
+ this.setupVisibilityHandler();
601
+ }
602
+ }
603
+ /**
604
+ * 推入新的 blocks
605
+ * 会自动过滤已存在的 blocks
606
+ */
607
+ push(blocks) {
608
+ const existingIds = this.getAllBlockIds();
609
+ const newBlocks = blocks.filter((b) => !existingIds.has(b.id));
610
+ if (newBlocks.length > 0) {
611
+ this.state.pendingBlocks.push(...newBlocks);
612
+ this.startIfNeeded();
613
+ }
614
+ if (this.state.currentBlock) {
615
+ const updated = blocks.find((b) => b.id === this.state.currentBlock.id);
616
+ if (updated && updated.node !== this.state.currentBlock.node) {
617
+ this.state.currentBlock = updated;
618
+ if (!this.rafId && !this.isPaused) {
619
+ const total = this.countChars(updated.node);
620
+ if (this.state.currentProgress < total) {
621
+ this.startIfNeeded();
622
+ }
623
+ }
624
+ }
625
+ }
626
+ }
627
+ /**
628
+ * 更新指定 block(用于 pending block 内容增加时)
629
+ */
630
+ update(block) {
631
+ if (this.state.currentBlock?.id === block.id) {
632
+ const oldTotal = this.countChars(this.state.currentBlock.node);
633
+ const newTotal = this.countChars(block.node);
634
+ this.state.currentBlock = block;
635
+ if (newTotal > oldTotal && !this.rafId && !this.isPaused && this.state.currentProgress >= oldTotal) {
636
+ this.startIfNeeded();
637
+ }
638
+ }
639
+ }
640
+ /**
641
+ * 跳过所有动画,直接显示全部内容
642
+ */
643
+ skip() {
644
+ this.stop();
645
+ const allBlocks = [
646
+ ...this.state.completedBlocks,
647
+ ...this.state.currentBlock ? [this.state.currentBlock] : [],
648
+ ...this.state.pendingBlocks
649
+ ];
650
+ this.state = {
651
+ completedBlocks: allBlocks,
652
+ currentBlock: null,
653
+ currentProgress: 0,
654
+ pendingBlocks: []
655
+ };
656
+ this.emit();
657
+ }
658
+ /**
659
+ * 重置状态
660
+ */
661
+ reset() {
662
+ this.stop();
663
+ this.state = {
664
+ completedBlocks: [],
665
+ currentBlock: null,
666
+ currentProgress: 0,
667
+ pendingBlocks: []
668
+ };
669
+ this.emit();
670
+ }
671
+ /**
672
+ * 暂停动画
673
+ */
674
+ pause() {
675
+ this.isPaused = true;
676
+ this.cancelRaf();
677
+ }
678
+ /**
679
+ * 恢复动画
680
+ */
681
+ resume() {
682
+ if (this.isPaused) {
683
+ this.isPaused = false;
684
+ this.startIfNeeded();
685
+ }
686
+ }
687
+ /**
688
+ * 获取用于渲染的 display blocks
689
+ */
690
+ getDisplayBlocks() {
691
+ const result = [];
692
+ for (const block of this.state.completedBlocks) {
693
+ result.push({
694
+ ...block,
695
+ displayNode: block.node,
696
+ progress: 1,
697
+ isDisplayComplete: true
698
+ });
699
+ }
700
+ if (this.state.currentBlock) {
701
+ const total = this.countChars(this.state.currentBlock.node);
702
+ const displayNode = this.sliceNode(this.state.currentBlock.node, this.state.currentProgress);
703
+ result.push({
704
+ ...this.state.currentBlock,
705
+ displayNode: displayNode || { type: "paragraph", children: [] },
706
+ progress: total > 0 ? this.state.currentProgress / total : 1,
707
+ isDisplayComplete: false
708
+ });
709
+ }
710
+ return result;
711
+ }
712
+ /**
713
+ * 是否正在处理中
714
+ */
715
+ isProcessing() {
716
+ return this.isRunning || this.state.currentBlock !== null || this.state.pendingBlocks.length > 0;
717
+ }
718
+ /**
719
+ * 是否已暂停
720
+ */
721
+ isPausedState() {
722
+ return this.isPaused;
723
+ }
724
+ /**
725
+ * 获取内部状态(用于调试)
726
+ */
727
+ getState() {
728
+ return { ...this.state };
729
+ }
730
+ /**
731
+ * 动态更新配置
732
+ */
733
+ setOptions(options) {
734
+ if (options.charsPerTick !== void 0) {
735
+ this.options.charsPerTick = options.charsPerTick;
736
+ }
737
+ if (options.tickInterval !== void 0) {
738
+ this.options.tickInterval = options.tickInterval;
739
+ }
740
+ if (options.effect !== void 0) {
741
+ this.options.effect = options.effect;
742
+ }
743
+ if (options.pauseOnHidden !== void 0) {
744
+ this.options.pauseOnHidden = options.pauseOnHidden;
745
+ if (options.pauseOnHidden && typeof document !== "undefined") {
746
+ this.setupVisibilityHandler();
747
+ } else {
748
+ this.removeVisibilityHandler();
749
+ }
750
+ }
751
+ }
752
+ /**
753
+ * 获取当前配置
754
+ */
755
+ getOptions() {
756
+ return {
757
+ charsPerTick: this.options.charsPerTick,
758
+ tickInterval: this.options.tickInterval,
759
+ effect: this.options.effect
760
+ };
761
+ }
762
+ /**
763
+ * 获取当前动画效果
764
+ */
765
+ getEffect() {
766
+ return this.options.effect;
767
+ }
768
+ /**
769
+ * 销毁,清理资源
770
+ */
771
+ destroy() {
772
+ this.stop();
773
+ this.removeVisibilityHandler();
774
+ }
775
+ // ============ 私有方法 ============
776
+ getAllBlockIds() {
777
+ return new Set([
778
+ ...this.state.completedBlocks.map((b) => b.id),
779
+ this.state.currentBlock?.id,
780
+ ...this.state.pendingBlocks.map((b) => b.id)
781
+ ].filter((id) => id !== void 0));
782
+ }
783
+ setupVisibilityHandler() {
784
+ if (this.visibilityHandler) return;
785
+ this.visibilityHandler = () => {
786
+ if (document.hidden) {
787
+ this.pause();
788
+ } else {
789
+ this.resume();
790
+ }
791
+ };
792
+ document.addEventListener("visibilitychange", this.visibilityHandler);
793
+ }
794
+ removeVisibilityHandler() {
795
+ if (this.visibilityHandler) {
796
+ document.removeEventListener("visibilitychange", this.visibilityHandler);
797
+ this.visibilityHandler = null;
798
+ }
799
+ }
800
+ startIfNeeded() {
801
+ if (this.rafId || this.isPaused) return;
802
+ if (!this.state.currentBlock && this.state.pendingBlocks.length > 0) {
803
+ this.state.currentBlock = this.state.pendingBlocks.shift();
804
+ this.state.currentProgress = 0;
805
+ }
806
+ if (this.state.currentBlock) {
807
+ this.isRunning = true;
808
+ this.lastTickTime = 0;
809
+ this.scheduleNextFrame();
810
+ }
811
+ }
812
+ scheduleNextFrame() {
813
+ this.rafId = requestAnimationFrame((time) => this.animationFrame(time));
814
+ }
815
+ animationFrame(time) {
816
+ this.rafId = null;
817
+ if (this.lastTickTime === 0) {
818
+ this.lastTickTime = time;
819
+ }
820
+ const elapsed = time - this.lastTickTime;
821
+ if (elapsed >= this.options.tickInterval) {
822
+ this.lastTickTime = time;
823
+ this.tick();
824
+ }
825
+ if (this.isRunning && !this.isPaused) {
826
+ this.scheduleNextFrame();
827
+ }
828
+ }
829
+ tick() {
830
+ const block = this.state.currentBlock;
831
+ if (!block) {
832
+ this.processNext();
833
+ return;
834
+ }
835
+ const total = this.countChars(block.node);
836
+ const step = this.getStep();
837
+ this.state.currentProgress = Math.min(this.state.currentProgress + step, total);
838
+ this.emit();
839
+ if (this.state.currentProgress >= total) {
840
+ this.notifyComplete(block.node);
841
+ this.state.completedBlocks.push(block);
842
+ this.state.currentBlock = null;
843
+ this.state.currentProgress = 0;
844
+ this.processNext();
845
+ }
846
+ }
847
+ getStep() {
848
+ const { charsPerTick } = this.options;
849
+ if (typeof charsPerTick === "number") {
850
+ return charsPerTick;
851
+ }
852
+ const [min, max] = charsPerTick;
853
+ return Math.floor(Math.random() * (max - min + 1)) + min;
854
+ }
855
+ processNext() {
856
+ if (this.state.pendingBlocks.length > 0) {
857
+ this.state.currentBlock = this.state.pendingBlocks.shift();
858
+ this.state.currentProgress = 0;
859
+ this.emit();
860
+ } else {
861
+ this.isRunning = false;
862
+ this.cancelRaf();
863
+ this.emit();
864
+ }
865
+ }
866
+ cancelRaf() {
867
+ if (this.rafId) {
868
+ cancelAnimationFrame(this.rafId);
869
+ this.rafId = null;
870
+ }
871
+ }
872
+ stop() {
873
+ this.cancelRaf();
874
+ this.isRunning = false;
875
+ this.isPaused = false;
876
+ }
877
+ emit() {
878
+ this.options.onChange(this.getDisplayBlocks());
879
+ }
880
+ // ============ 插件调用 ============
881
+ countChars(node) {
882
+ for (const plugin of this.options.plugins) {
883
+ if (plugin.match?.(node) && plugin.countChars) {
884
+ const result = plugin.countChars(node);
885
+ if (result !== void 0) return result;
886
+ }
887
+ }
888
+ return countChars(node);
889
+ }
890
+ sliceNode(node, chars) {
891
+ for (const plugin of this.options.plugins) {
892
+ if (plugin.match?.(node) && plugin.sliceNode) {
893
+ const total = this.countChars(node);
894
+ const result = plugin.sliceNode(node, chars, total);
895
+ if (result !== null) return result;
896
+ }
897
+ }
898
+ return sliceAst(node, chars);
899
+ }
900
+ notifyComplete(node) {
901
+ for (const plugin of this.options.plugins) {
902
+ if (plugin.match?.(node) && plugin.onComplete) {
903
+ plugin.onComplete(node);
904
+ }
905
+ }
906
+ }
907
+ };
908
+ function createBlockTransformer(options) {
909
+ return new BlockTransformer(options);
910
+ }
911
+
912
+ // src/transformer/plugins.ts
913
+ var codeBlockPlugin = {
914
+ name: "code-block",
915
+ match: (node) => node.type === "code",
916
+ countChars: () => 1,
917
+ // 算作 1 个字符,整体出现
918
+ sliceNode: (node, displayedChars, totalChars) => {
919
+ return displayedChars >= totalChars ? node : null;
920
+ }
921
+ };
922
+ var mermaidPlugin = {
923
+ name: "mermaid",
924
+ match: (node) => {
925
+ if (node.type !== "code") return false;
926
+ const codeNode = node;
927
+ return codeNode.lang === "mermaid";
928
+ },
929
+ countChars: () => 1,
930
+ sliceNode: (node, displayedChars) => displayedChars > 0 ? node : null
931
+ };
932
+ var imagePlugin = {
933
+ name: "image",
934
+ match: (node) => node.type === "image",
935
+ countChars: () => 0
936
+ // 0 字符,立即显示
937
+ };
938
+ var mathPlugin = {
939
+ name: "math",
940
+ match: (node) => {
941
+ const type = node.type;
942
+ return type === "math" || type === "inlineMath";
943
+ },
944
+ countChars: () => 1,
945
+ sliceNode: (node, displayedChars) => displayedChars > 0 ? node : null
946
+ };
947
+ var thematicBreakPlugin = {
948
+ name: "thematic-break",
949
+ match: (node) => node.type === "thematicBreak",
950
+ countChars: () => 0
951
+ };
952
+ var defaultPlugins = [
953
+ imagePlugin,
954
+ thematicBreakPlugin
955
+ ];
956
+ var allPlugins = [
957
+ mermaidPlugin,
958
+ // mermaid 优先于普通 code block
959
+ codeBlockPlugin,
960
+ imagePlugin,
961
+ mathPlugin,
962
+ thematicBreakPlugin
963
+ ];
964
+ function createPlugin(name, matcher, options = {}) {
965
+ return {
966
+ name,
967
+ match: matcher,
968
+ ...options
969
+ };
970
+ }
971
+
972
+ 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
973
  //# sourceMappingURL=index.js.map
525
974
  //# sourceMappingURL=index.js.map