diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts
new file mode 100644
index 0000000..6e3ac66
--- /dev/null
+++ b/src/__tests__/index.test.ts
@@ -0,0 +1,48 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import { CacheInterface, HTMLPagination } from "../index";
+
+describe("HTMLPagination", () => {
+ let hp: HTMLPagination;
+
+ const content = document.createElement("div");
+ const container = document.createElement("div");
+ class Cache extends CacheInterface {
+ g(key: string) {
+ return localStorage.getItem(key);
+ }
+ s(key: string, value: string) {
+ localStorage.setItem(key, value);
+ }
+ }
+
+ beforeEach(() => {
+ content.innerHTML = "";
+ content.innerHTML = "
";
+
+ container.innerHTML = "";
+
+ hp = new HTMLPagination(content, container, new Cache());
+ });
+
+ test("Computes positions for each text node", () => {
+ expect(hp.elementPositions.map((el) => el[0])).toEqual([0, 2, 4, 6]);
+ });
+
+ test("Gets element by position", () => {
+ expect(hp.getElementForPosition(0)[0]).toBe(0);
+ expect(hp.getElementForPosition(1)[0]).toBe(0);
+ expect(hp.getElementForPosition(2)[0]).toBe(2);
+ expect(hp.getElementForPosition(3)[0]).toBe(2);
+ });
+
+ test("Gets html content for range of text", () => {
+ expect(hp.getFromRange(0, 8)).toEqual(
+ "aa
bbccdd
"
+ );
+ expect(hp.getFromRange(0, 3)).toEqual("aa
b
");
+ expect(hp.getFromRange(5, 7)).toEqual("cd");
+ });
+});
diff --git a/src/cache.ts b/src/cache.ts
new file mode 100644
index 0000000..6283875
--- /dev/null
+++ b/src/cache.ts
@@ -0,0 +1,9 @@
+/**
+ * Interface to any storage which supports key-value storage
+ */
+export abstract class CacheInterface {
+ /** Get */
+ abstract g(key: string): string | null;
+ /** Set */
+ abstract s(key: string, value: string): void;
+}
diff --git a/src/index.ts b/src/index.ts
index fe2a725..f4965d0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,22 +1,100 @@
-export declare abstract class CacheInterface {
- abstract g(key: string): string;
- abstract s(key: string, value: string): void;
+import { CacheInterface } from "./cache";
+import { isElementNode, isTextNode, TextNode } from "./types";
+
+class HTMLPagination {
+ content: HTMLElement;
+ container: HTMLElement;
+ cache: CacheInterface;
+
+ elementPositions: [number, TextNode][];
+ idPositions: Map;
+
+ /**
+ * @param content HTML element with html content to display paginationly
+ * @param container HTML element which will store content to display
+ * @param cache Class implementing `g` and `s` methods for getting and setting elements of KV storage
+ */
+ constructor(
+ content: HTMLElement,
+ container: HTMLElement,
+ cache: CacheInterface
+ ) {
+ this.content = content;
+ this.container = container;
+ this.cache = cache;
+
+ this.elementPositions = new Array();
+ this.idPositions = new Map();
+
+ this.computeElementsPositions();
+ }
+
+ getPage(n: number): string {
+ const from = 0;
+ const to = 1;
+
+ return this.getFromRange(from, to);
+ }
+
+ /**
+ * Computes html elements and text nodes positions. Must be run only on first setup
+ */
+ computeElementsPositions(): void {
+ const recursive = (currentPosition: number, root: Node): number => {
+ if (isTextNode(root)) {
+ this.elementPositions.push([currentPosition, root]);
+
+ return currentPosition + root.nodeValue.length;
+ } else if (isElementNode(root)) {
+ if (root.id !== null) this.idPositions.set(root.id, currentPosition);
+
+ return Array.from(root.childNodes).reduce(recursive, currentPosition);
+ } else {
+ return currentPosition;
+ }
+ };
+
+ recursive(0, this.content);
+ }
+
+ /**
+ * Finds node inside which `pos` is located. Returns node positions and itself
+ */
+ getElementForPosition(pos: number): [number, Node] {
+ let s = 0,
+ e = this.elementPositions.length - 1;
+
+ while (s <= e) {
+ const c = (s + e) >> 1;
+ if (pos > this.elementPositions[c][0]) s = c + 1;
+ else if (pos < this.elementPositions[c][0]) e = c - 1;
+ else return this.elementPositions[c];
+ }
+
+ return this.elementPositions[s - 1];
+ }
+
+ /**
+ * Sets `container` element content and return as string html content between `from` and `to`
+ */
+ getFromRange(from: number, to: number): string {
+ this.container.innerHTML = "";
+ const range = new Range();
+
+ const [startPosition, startElement] = this.getElementForPosition(from);
+ const startOffset = from - startPosition;
+ range.setStart(startElement, startOffset);
+
+ const [endPosition, endElement] = this.getElementForPosition(to);
+ const endOffset = to - endPosition;
+ range.setEnd(endElement, endOffset);
+
+ // TODO: copy range with all its parent elements
+
+ this.container.appendChild(range.cloneContents());
+
+ return this.container.innerHTML;
+ }
}
-/**
- * Function to get page with specific number
- * @param n Page number
- */
-declare function getPage(n: number): string;
-
-/**
- * Function to prepare unific data and compose page content getting function
- * @param content HTML element with html content to display paginationly
- * @param container HTML element which will store content to display
- * @param cache Class implementing `g` and `s` methods for getting and setting elements of KV storage
- */
-export declare function setup(
- content: HTMLElement,
- container: HTMLElement,
- cache: CacheInterface
-): typeof getPage;
+export { CacheInterface, HTMLPagination };
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 0000000..b0f9685
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,7 @@
+export type TextNode = Node & { nodeValue: string };
+
+export const isTextNode = (el: Node): el is TextNode =>
+ el.nodeType === Node.TEXT_NODE;
+
+export const isElementNode = (el: Node): el is HTMLElement =>
+ el.nodeType === Node.ELEMENT_NODE;