@likecoin/epub-ts 0.5.1 → 0.6.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.
@@ -2,6 +2,7 @@ import { default as Mapping } from '../../mapping';
2
2
  import { default as Queue } from '../../utils/queue';
3
3
  import { default as Stage } from '../helpers/stage';
4
4
  import { default as Views } from '../helpers/views';
5
+ import { default as TextMeasurer } from '../../utils/text-measurer';
5
6
  import { default as Layout } from '../../layout';
6
7
  import { default as Section } from '../../section';
7
8
  import { default as Contents } from '../../contents';
@@ -52,6 +53,7 @@ declare class DefaultViewManager implements IEventEmitter<DefaultManagerEvents>
52
53
  overflow: string;
53
54
  layout: Layout;
54
55
  mapping: Mapping;
56
+ _measurer: TextMeasurer;
55
57
  location: ViewLocation[];
56
58
  isPaginated: boolean;
57
59
  scrollLeft: number;
@@ -52,6 +52,7 @@ declare class IframeView implements IEventEmitter<IframeViewEvents> {
52
52
  _textHeight: number | undefined;
53
53
  _contentWidth: number | undefined;
54
54
  _contentHeight: number | undefined;
55
+ _contentDirty: boolean;
55
56
  _needsReframe: boolean;
56
57
  _expanding: boolean;
57
58
  elementBounds: {
package/dist/mapping.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { EpubCFIPair, RangePair, LayoutProps } from './types';
2
2
  import { default as IframeView } from './managers/views/iframe';
3
3
  import { default as Contents } from './contents';
4
+ import { default as TextMeasurer } from './utils/text-measurer';
4
5
  /**
5
6
  * Map text locations to CFI ranges
6
7
  * @class
@@ -14,7 +15,8 @@ declare class Mapping {
14
15
  horizontal: boolean;
15
16
  direction: string;
16
17
  _dev: boolean;
17
- constructor(layout: LayoutProps, direction?: string, axis?: string, dev?: boolean);
18
+ _measurer: TextMeasurer | null;
19
+ constructor(layout: LayoutProps, direction?: string, axis?: string, dev?: boolean, measurer?: TextMeasurer);
18
20
  /**
19
21
  * Find CFI pairs for entire section at once
20
22
  */
@@ -54,24 +56,39 @@ declare class Mapping {
54
56
  * @return {Range}
55
57
  */
56
58
  findEnd(root: Node, start: number, end: number): Range;
59
+ /**
60
+ * Try to prepare a text node's root for canvas-based measurement.
61
+ * Returns the PreparedNode for this text node, or null if not available.
62
+ * @private
63
+ */
64
+ private _canvasPrepare;
65
+ /**
66
+ * Canvas fast path: use binary search on pre-measured cumulative widths
67
+ * to find a Range at the target position, then verify with one getBoundingClientRect.
68
+ * Returns the Range if verification passes, or null to fall through to DOM loop.
69
+ * @private
70
+ */
71
+ private _canvasFindRange;
57
72
  /**
58
73
  * Find Text Start Range
59
74
  * @private
60
- * @param {Node} root root node
75
+ * @param {Node} node text node
61
76
  * @param {number} start position to start at
62
77
  * @param {number} end position to end at
78
+ * @param {DOMRect} [nodePos] pre-computed node bounds from findStart (avoids redundant reflow)
63
79
  * @return {Range}
64
80
  */
65
- findTextStartRange(node: Node, start: number, end: number): Range;
81
+ findTextStartRange(node: Node, start: number, end: number, nodePos?: DOMRect): Range;
66
82
  /**
67
83
  * Find Text End Range
68
84
  * @private
69
- * @param {Node} root root node
85
+ * @param {Node} node text node
70
86
  * @param {number} start position to start at
71
87
  * @param {number} end position to end at
88
+ * @param {DOMRect} [nodePos] pre-computed node bounds from findEnd (avoids redundant reflow)
72
89
  * @return {Range}
73
90
  */
74
- findTextEndRange(node: Node, start: number, end: number): Range;
91
+ findTextEndRange(node: Node, start: number, end: number, nodePos?: DOMRect): Range;
75
92
  /**
76
93
  * Split up a text node into ranges for each word
77
94
  * @private
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Canvas-based text measurement utility.
3
+ *
4
+ * Applies pretext's prepare/layout pattern: expensive measurement is done once
5
+ * via CanvasRenderingContext2D.measureText(), then layout queries (finding the
6
+ * character offset at a pixel position) are pure binary-search arithmetic with
7
+ * zero DOM reflow.
8
+ *
9
+ * Browser-only — not imported by the Node.js entry point.
10
+ */
11
+ export interface TextSegment {
12
+ /** The text node this segment belongs to */
13
+ node: Text;
14
+ /** Character offset within the text node where this segment starts */
15
+ charOffset: number;
16
+ /** The segment text content */
17
+ text: string;
18
+ /** Measured width of this segment in pixels */
19
+ width: number;
20
+ /** Cumulative width from the start of this text node's segment list */
21
+ cumWidth: number;
22
+ }
23
+ export interface PreparedNode {
24
+ node: Text;
25
+ segments: TextSegment[];
26
+ totalWidth: number;
27
+ font: string;
28
+ }
29
+ declare class TextMeasurer {
30
+ private _canvas;
31
+ private _ctx;
32
+ /** font string → (text → width), bounded to MAX_WIDTH_CACHE_FONTS entries */
33
+ private _widthCache;
34
+ /** parent element → prepared nodes */
35
+ private _preparedCache;
36
+ /** text node → prepared node, for O(1) lookup in _canvasPrepare */
37
+ private _nodeIndex;
38
+ /** shared Intl.Segmenter instance (lazy) */
39
+ private _segmenter;
40
+ private getCanvas;
41
+ private getSegmenter;
42
+ /**
43
+ * Measure a text string with a given CSS font, returning its width in pixels.
44
+ * Results are cached per font+text pair.
45
+ */
46
+ measureText(text: string, font: string): number;
47
+ /**
48
+ * Segment text into word-level pieces suitable for measurement.
49
+ * Uses Intl.Segmenter when available, falls back to space-splitting
50
+ * (with per-character splitting for CJK).
51
+ */
52
+ segmentText(text: string): {
53
+ text: string;
54
+ index: number;
55
+ }[];
56
+ /**
57
+ * Prepare phase: measure all text nodes under a root element.
58
+ * Returns PreparedNode[] with cumulative widths for binary search.
59
+ *
60
+ * Text nodes whose parent has exotic CSS (letter-spacing, word-spacing,
61
+ * text-indent) are skipped — the caller should fall back to DOM Range
62
+ * measurement for those.
63
+ *
64
+ * @param root The container element (usually document.body)
65
+ * @param win The window object for getComputedStyle
66
+ * @returns PreparedNode[] with entries for measurable text nodes (may be empty)
67
+ */
68
+ prepare(root: Element, win: Window): PreparedNode[];
69
+ /**
70
+ * Layout phase: find the segment index at a given pixel position
71
+ * using binary search on cumulative widths.
72
+ *
73
+ * @param segments The TextSegment[] from a PreparedNode
74
+ * @param position Target position in pixels (relative to text node start)
75
+ * @returns Index into the segments array
76
+ */
77
+ findSegmentIndex(segments: TextSegment[], position: number): number;
78
+ /**
79
+ * Look up a previously prepared text node in O(1).
80
+ * Returns null if the node was not prepared (exotic CSS, not yet prepared, etc.).
81
+ */
82
+ getPreparedNode(node: Text): PreparedNode | null;
83
+ /**
84
+ * Check if a text node's parent has exotic CSS that prevents canvas measurement.
85
+ */
86
+ hasExoticCSS(node: Text, win: Window): boolean;
87
+ /**
88
+ * Invalidate cached preparation for a root element,
89
+ * including all per-node index entries under it.
90
+ */
91
+ invalidate(root: Element): void;
92
+ /**
93
+ * Destroy the measurer, releasing the canvas and all caches.
94
+ */
95
+ destroy(): void;
96
+ }
97
+ export default TextMeasurer;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@likecoin/epub-ts",
3
- "version": "0.5.1",
4
- "description": "TypeScript EPUB parser and renderer, forked from epubjs",
3
+ "version": "0.6.0",
4
+ "description": "Drop-in epubjs replacement fully typed, 1 dependency, actively maintained",
5
5
  "keywords": [
6
6
  "epub",
7
7
  "ebook",
@@ -76,6 +76,10 @@
76
76
  "optional": true
77
77
  }
78
78
  },
79
+ "publishConfig": {
80
+ "access": "public",
81
+ "provenance": true
82
+ },
79
83
  "dependencies": {
80
84
  "jszip": "^3.10.1"
81
85
  }