@plannotator/web-highlighter 0.8.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.
Files changed (88) hide show
  1. package/.cursor/environment.json +6 -0
  2. package/.eslintrc.js +250 -0
  3. package/.prettierrc +9 -0
  4. package/.travis.yml +17 -0
  5. package/CHANGELOG.md +220 -0
  6. package/LICENSE +21 -0
  7. package/README.md +371 -0
  8. package/README.zh_CN.md +367 -0
  9. package/config/base.config.js +25 -0
  10. package/config/base.example.config.js +38 -0
  11. package/config/paths.js +22 -0
  12. package/config/server.config.js +17 -0
  13. package/config/webpack.config.dev.js +18 -0
  14. package/config/webpack.config.example.js +20 -0
  15. package/config/webpack.config.prod.js +28 -0
  16. package/dist/data/cache.d.ts +13 -0
  17. package/dist/index.d.ts +58 -0
  18. package/dist/model/range/dom.d.ts +6 -0
  19. package/dist/model/range/index.d.ts +20 -0
  20. package/dist/model/range/selection.d.ts +14 -0
  21. package/dist/model/source/dom.d.ts +23 -0
  22. package/dist/model/source/index.d.ts +18 -0
  23. package/dist/painter/dom.d.ts +22 -0
  24. package/dist/painter/index.d.ts +19 -0
  25. package/dist/painter/style.d.ts +1 -0
  26. package/dist/types/index.d.ts +102 -0
  27. package/dist/util/camel.d.ts +5 -0
  28. package/dist/util/const.d.ts +41 -0
  29. package/dist/util/deferred.d.ts +9 -0
  30. package/dist/util/dom.d.ts +32 -0
  31. package/dist/util/event.emitter.d.ts +13 -0
  32. package/dist/util/hook.d.ts +15 -0
  33. package/dist/util/interaction.d.ts +6 -0
  34. package/dist/util/is.mobile.d.ts +5 -0
  35. package/dist/util/tool.d.ts +4 -0
  36. package/dist/util/uuid.d.ts +4 -0
  37. package/dist/web-highlighter.min.js +3 -0
  38. package/dist/web-highlighter.min.js.map +1 -0
  39. package/docs/ADVANCE.md +113 -0
  40. package/docs/ADVANCE.zh_CN.md +111 -0
  41. package/docs/img/create-flow.jpg +0 -0
  42. package/docs/img/create-flow.zh_CN.jpg +0 -0
  43. package/docs/img/logo.png +0 -0
  44. package/docs/img/remove-flow.jpg +0 -0
  45. package/docs/img/remove-flow.zh_CN.jpg +0 -0
  46. package/docs/img/sample.gif +0 -0
  47. package/example/index.css +2 -0
  48. package/example/index.js +214 -0
  49. package/example/local.store.js +72 -0
  50. package/example/my.css +119 -0
  51. package/example/tpl.html +59 -0
  52. package/package.json +103 -0
  53. package/script/build.js +17 -0
  54. package/script/convet-md.js +25 -0
  55. package/script/dev.js +22 -0
  56. package/src/data/cache.ts +57 -0
  57. package/src/index.ts +285 -0
  58. package/src/model/range/dom.ts +94 -0
  59. package/src/model/range/index.ts +88 -0
  60. package/src/model/range/selection.ts +55 -0
  61. package/src/model/source/dom.ts +66 -0
  62. package/src/model/source/index.ts +54 -0
  63. package/src/painter/dom.ts +345 -0
  64. package/src/painter/index.ts +199 -0
  65. package/src/painter/style.ts +21 -0
  66. package/src/types/index.ts +118 -0
  67. package/src/util/camel.ts +6 -0
  68. package/src/util/const.ts +54 -0
  69. package/src/util/deferred.ts +37 -0
  70. package/src/util/dom.ts +155 -0
  71. package/src/util/event.emitter.ts +45 -0
  72. package/src/util/hook.ts +52 -0
  73. package/src/util/interaction.ts +20 -0
  74. package/src/util/is.mobile.ts +7 -0
  75. package/src/util/tool.ts +14 -0
  76. package/src/util/uuid.ts +10 -0
  77. package/test/api.spec.ts +555 -0
  78. package/test/event.spec.ts +284 -0
  79. package/test/fixtures/broken.json +32 -0
  80. package/test/fixtures/index.html +11 -0
  81. package/test/fixtures/source.json +47 -0
  82. package/test/hook.spec.ts +244 -0
  83. package/test/integrate.spec.ts +48 -0
  84. package/test/mobile.spec.ts +87 -0
  85. package/test/option.spec.ts +212 -0
  86. package/test/util.spec.ts +244 -0
  87. package/test-newlines.html +226 -0
  88. package/tsconfig.json +23 -0
@@ -0,0 +1,284 @@
1
+ import { expect } from 'chai';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import jsdomGlobal from 'jsdom-global';
5
+ import jsdom from 'jsdom';
6
+ import Highlighter from '../src/index';
7
+ import { CreateFrom } from '../src/types/index';
8
+ import HighlightSource from '../src/model/source/index';
9
+ import sources from './fixtures/source.json';
10
+ import sinon from 'sinon';
11
+
12
+ describe('Event Emit', function () {
13
+ this.timeout(50000);
14
+
15
+ let highlighter: Highlighter;
16
+ let cleanup;
17
+
18
+ beforeEach(() => {
19
+ const html = readFileSync(resolve(__dirname, 'fixtures', 'index.html'), 'utf-8');
20
+ cleanup = jsdomGlobal('', {
21
+ resources: new jsdom.ResourceLoader({
22
+ userAgent: 'Mellblomenator/9000'
23
+ })
24
+ });
25
+ document.body.innerHTML = html;
26
+ highlighter = new Highlighter();
27
+ });
28
+
29
+ describe('#CREATE event', () => {
30
+ let listener: sinon.SinonSpy<any[], any[]>;
31
+
32
+ beforeEach(() => {
33
+ listener = sinon.spy();
34
+ (highlighter as any).on(Highlighter.event.CREATE, listener);
35
+ });
36
+
37
+ it('should be emitted by calling .fromRange', () => {
38
+ const range = document.createRange();
39
+ const $p = document.querySelectorAll('p')[0];
40
+ range.setStart($p.childNodes[0], 0);
41
+ range.setEnd($p.childNodes[0], 17);
42
+ highlighter.fromRange(range);
43
+
44
+ expect(listener.calledOnce).to.be.true;
45
+ });
46
+
47
+ it('should get correct arguments by calling .fromRange', () => {
48
+ const range = document.createRange();
49
+ const $p = document.querySelectorAll('p')[0];
50
+ range.setStart($p.childNodes[0], 0);
51
+ range.setEnd($p.childNodes[0], 17);
52
+ const content = range.toString();
53
+ highlighter.fromRange(range);
54
+ const sources: HighlightSource[] = listener.args[0][0].sources;
55
+
56
+ expect(listener.args[0][0].type).to.be.equal(CreateFrom.INPUT);
57
+ expect(sources[0].text).to.be.equal(content);
58
+ expect(Highlighter.isHighlightSource(sources[0])).to.be.true;
59
+ });
60
+
61
+ it('should be emitted by calling .fromStore', () => {
62
+ const s = sources[0];
63
+ highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id);
64
+
65
+ expect(listener.calledOnce).to.be.true;
66
+ });
67
+
68
+ it('should get correct arguments by calling .fromStore', () => {
69
+ const s = sources[0];
70
+ highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id);
71
+ const sourcesRes: HighlightSource[] = listener.args[0][0].sources;
72
+
73
+ expect(listener.args[0][0].type).to.be.equal(CreateFrom.STORE);
74
+ expect(sourcesRes[0].text).to.be.equal(s.text);
75
+ expect(Highlighter.isHighlightSource(sourcesRes[0])).to.be.true;
76
+ });
77
+
78
+ it('should be emitted when running automatically', () => {
79
+ highlighter.run();
80
+
81
+ const range = document.createRange();
82
+ const $p = document.querySelectorAll('p')[0];
83
+ range.setStart($p.childNodes[0], 0);
84
+ range.setEnd($p.childNodes[0], 17);
85
+ window.getSelection().addRange(range);
86
+
87
+ const e = new MouseEvent('mouseup', {
88
+ view: window,
89
+ bubbles: true,
90
+ cancelable: true
91
+ });
92
+ document.body.dispatchEvent(e);
93
+
94
+ expect(listener.calledOnce).to.be.true;
95
+ });
96
+
97
+ it('should get correct arguments when running automatically', () => {
98
+ highlighter.run();
99
+
100
+ const range = document.createRange();
101
+ const $p = document.querySelectorAll('p')[0];
102
+ range.setStart($p.childNodes[0], 0);
103
+ range.setEnd($p.childNodes[0], 17);
104
+ window.getSelection().addRange(range);
105
+
106
+ const content = range.toString();
107
+ const e = new MouseEvent('mouseup', {
108
+ view: window,
109
+ bubbles: true,
110
+ cancelable: true
111
+ });
112
+ document.body.dispatchEvent(e);
113
+
114
+ const sources: HighlightSource[] = listener.args[0][0].sources;
115
+ expect(listener.args[0][0].type).to.be.equal(CreateFrom.INPUT);
116
+ expect(sources[0].text).to.be.equal(content);
117
+ expect(Highlighter.isHighlightSource(sources[0])).to.be.true;
118
+ });
119
+ });
120
+
121
+ describe('#REMOVE event', () => {
122
+ let listener: sinon.SinonSpy<any[], any[]>;
123
+
124
+ beforeEach(() => {
125
+ highlighter.removeAll();
126
+ sources.forEach(s => highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id));
127
+ listener = sinon.spy();
128
+ (highlighter as any).on(Highlighter.event.REMOVE, listener);
129
+ });
130
+
131
+ it('should be emitted by calling .removeAll', () => {
132
+ highlighter.removeAll();
133
+ expect(listener.calledOnce).to.be.true;
134
+ });
135
+
136
+ it('should get correct ids as arguments by calling .removeAll', () => {
137
+ highlighter.removeAll();
138
+ const ids: string[] = listener.args[0][0].ids;
139
+ expect(ids).lengthOf(sources.length);
140
+ expect(ids.every(id => sources.map(o => o.id).indexOf(id) > -1)).to.be.true;
141
+ expect(listener.calledOnce).to.be.true;
142
+ });
143
+
144
+ it('should be emitted by calling .remove', () => {
145
+ highlighter.remove(sources[0].id);
146
+ expect(listener.calledOnce).to.be.true;
147
+ });
148
+
149
+ it('should get correct ids as arguments by calling .remove', () => {
150
+ const id = sources[0].id
151
+ highlighter.remove(id);
152
+ const ids: string[] = listener.args[0][0].ids;
153
+ expect(ids).lengthOf(1);
154
+ expect(ids[0]).to.be.equal(id);
155
+ });
156
+
157
+ it('should be emitted when the id does not exist', () => {
158
+ highlighter.remove('fake id');
159
+ expect(listener.calledOnce).to.be.false;
160
+ });
161
+ });
162
+
163
+ describe('#CLICK event', () => {
164
+ let listener: sinon.SinonSpy<any[], any[]>;
165
+ let event: MouseEvent;
166
+
167
+ beforeEach(() => {
168
+ highlighter.removeAll();
169
+ sources.forEach(s => highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id));
170
+ listener = sinon.spy();
171
+ (highlighter as any).on(Highlighter.event.CLICK, listener);
172
+ event = new MouseEvent('click', {
173
+ view: window,
174
+ bubbles: true,
175
+ cancelable: true
176
+ });
177
+ });
178
+
179
+ it('should be emitted correctly', () => {
180
+ highlighter.getDoms(sources[0].id)[0].dispatchEvent(event);
181
+ expect(listener.calledOnce).to.be.true;
182
+ });
183
+
184
+ it('should get correct id as arguments', () => {
185
+ const id = sources[0].id;
186
+ highlighter.getDoms(id)[0].dispatchEvent(event);
187
+ const args = listener.args[0][0];
188
+
189
+ expect(args.id).to.be.equal(id);
190
+ });
191
+
192
+ it('should not be emitted when clicked dom is not a wrapper', () => {
193
+ document.querySelector('p').dispatchEvent(event);
194
+ expect(listener.calledOnce).to.be.false;
195
+ });
196
+ });
197
+
198
+ describe('#HOVER event', () => {
199
+ let listener: sinon.SinonSpy<any[], any[]>;
200
+ let event: MouseEvent;
201
+
202
+ beforeEach(() => {
203
+ highlighter.removeAll();
204
+ sources.forEach(s => highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id));
205
+ listener = sinon.spy();
206
+ (highlighter as any).on(Highlighter.event.HOVER, listener);
207
+ event = new MouseEvent('mouseover', {
208
+ view: window,
209
+ bubbles: true,
210
+ cancelable: true
211
+ });
212
+ });
213
+
214
+ it('should be emitted correctly', () => {
215
+ highlighter.getDoms(sources[0].id)[0].dispatchEvent(event);
216
+ expect(listener.calledOnce).to.be.true;
217
+ });
218
+
219
+ it('should get correct id as arguments', () => {
220
+ const id = sources[0].id;
221
+ highlighter.getDoms(id)[0].dispatchEvent(event);
222
+ const args = listener.args[0][0];
223
+
224
+ expect(args.id).to.be.equal(id);
225
+ });
226
+
227
+ it('should not be emitted when the dom hovered is not a wrapper', () => {
228
+ document.querySelector('p').dispatchEvent(event);
229
+ expect(listener.calledOnce).to.be.false;
230
+ });
231
+
232
+ it('should not be emitted when move to a same highlighted wrapper', () => {
233
+ highlighter.getDoms(sources[1].id)[0].dispatchEvent(event);
234
+ highlighter.getDoms(sources[1].id)[1].dispatchEvent(event);
235
+ expect(listener.calledOnce).to.be.true;
236
+ });
237
+ });
238
+
239
+ describe('#HOVER_OUT event', () => {
240
+ let listener: sinon.SinonSpy<any[], any[]>;
241
+ let event: MouseEvent;
242
+
243
+ beforeEach(() => {
244
+ highlighter.removeAll();
245
+ sources.forEach(s => highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id));
246
+ listener = sinon.spy();
247
+ (highlighter as any).on(Highlighter.event.HOVER_OUT, listener);
248
+ event = new MouseEvent('mouseover', {
249
+ view: window,
250
+ bubbles: true,
251
+ cancelable: true
252
+ });
253
+ });
254
+
255
+ it('should be emitted correctly', () => {
256
+ highlighter.getDoms(sources[0].id)[0].dispatchEvent(event);
257
+ document.querySelector('p').dispatchEvent(event);
258
+ expect(listener.calledOnce).to.be.true;
259
+ });
260
+
261
+ it('should get correct id as arguments', () => {
262
+ const id = sources[0].id;
263
+ highlighter.getDoms(sources[0].id)[0].dispatchEvent(event);
264
+ highlighter.getDoms(sources[1].id)[0].dispatchEvent(event);
265
+
266
+ expect(listener.args[0][0].id).to.be.equal(id);
267
+ });
268
+
269
+ it('should not be emitted when just hover a wrapper', () => {
270
+ highlighter.getDoms(sources[0].id)[0].dispatchEvent(event);
271
+ expect(listener.calledOnce).to.be.false;
272
+ });
273
+
274
+ it('should not be emitted when just move to a wrapper in the same highlight', () => {
275
+ highlighter.getDoms(sources[1].id)[0].dispatchEvent(event);
276
+ highlighter.getDoms(sources[1].id)[1].dispatchEvent(event);
277
+ expect(listener.calledOnce).to.be.false;
278
+ });
279
+ });
280
+
281
+ afterEach(() => {
282
+ cleanup();
283
+ });
284
+ });
@@ -0,0 +1,32 @@
1
+ [
2
+ {
3
+ "startMeta": {
4
+ "parentTagName": "P",
5
+ "parentIndex": 0,
6
+ "textOffset": 0
7
+ },
8
+ "endMeta": {
9
+ "parentTagName": "CODE",
10
+ "parentIndex": 0,
11
+ "textOffset": 5
12
+ },
13
+ "text": "It's a broken source",
14
+ "id": "238bec32-693c-4e59-afe6-670f1a644ca2",
15
+ "__isHighlightSource": {}
16
+ },
17
+ {
18
+ "startMeta": {
19
+ "parentTagName": "P",
20
+ "parentIndex": 0,
21
+ "textOffset": 0
22
+ },
23
+ "endMeta": {
24
+ "parentTagName": "CODE",
25
+ "parentIndex": 0,
26
+ "textOffset": 5
27
+ },
28
+ "text": "It's a broken source",
29
+ "id": "238bec32-693c-4e59-afe6-670f1a644ca2",
30
+ "__isHighlightSource": {}
31
+ }
32
+ ]
@@ -0,0 +1,11 @@
1
+ <main>
2
+ <h1>Web Highlighter</h1>
3
+ <h2>Background</h2>
4
+ <p id="js-first-p" class="first-p">It's from an idea: highlight texts on the website and save the highlighted areas just like what you do in PDF.</p>
5
+ <p>If you have ever visited <a href="http://medium.com">medium.com</a>, you must know the feature of highlighting notes: users select a text segment and click the 'highlight' button. Then the text will be highlighted with a shining background color. Besides, the highlighted areas will be saved and recovered when you visit it next time. It's like the simple demo bellow.</p>
6
+ <p><img src="https://raw.githubusercontent.com/alienzhou/web-highlighter/master/docs/img/sample.gif" alt=""/></p>
7
+ <p>This is a useful feature for readers.<span class="highlight-mengshou-wrap" data-highlight-id="8a4cf231-6601-49f2-9137-e7677d592312" data-highlight-split-type="both" data-highlight-id-extra="">If you're a developer, you may want your website support it and attract more visits.</span>If you're a user (like me), you may want a browser-plugin to do this.</p>
8
+ <p>For this reason, the repo (web-highlighter) aims to help you implement highlighting-note on any website quickly (e.g. blogs, document viewers, online books and so on). It contains the core abilities for note highlighting and persistence. And you can implement your own product by some easy-to-use APIs. It has been used for our sites in production.</p>
9
+ <p>Try it and<br>have fun~</p>
10
+ </main>
11
+ <footer>This is a footer</footer>
@@ -0,0 +1,47 @@
1
+ [
2
+ {
3
+ "startMeta": {
4
+ "parentTagName": "P",
5
+ "parentIndex": 0,
6
+ "textOffset": 0
7
+ },
8
+ "endMeta": {
9
+ "parentTagName": "P",
10
+ "parentIndex": 0,
11
+ "textOffset": 17
12
+ },
13
+ "text": "It's from an idea",
14
+ "id": "cbfcf177-4a0e-4deb-9f00-66e247ee5363",
15
+ "__isHighlightSource": {}
16
+ },
17
+ {
18
+ "startMeta": {
19
+ "parentTagName": "P",
20
+ "parentIndex": 0,
21
+ "textOffset": 10
22
+ },
23
+ "endMeta": {
24
+ "parentTagName": "P",
25
+ "parentIndex": 0,
26
+ "textOffset": 34
27
+ },
28
+ "text": "an idea: highlight texts",
29
+ "id": "69a33f5e-60f5-4b5d-9f9c-18151a24bf95",
30
+ "__isHighlightSource": {}
31
+ },
32
+ {
33
+ "startMeta": {
34
+ "parentTagName": "P",
35
+ "parentIndex": 0,
36
+ "textOffset": 8
37
+ },
38
+ "endMeta": {
39
+ "parentTagName": "P",
40
+ "parentIndex": 0,
41
+ "textOffset": 26
42
+ },
43
+ "text": "m an idea: highlig",
44
+ "id": "14206065-205f-4c6d-8f80-1e022a6695a3",
45
+ "__isHighlightSource": {}
46
+ }
47
+ ]
@@ -0,0 +1,244 @@
1
+ import { expect } from 'chai';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import jsdomGlobal from 'jsdom-global';
5
+ import Highlighter from '../src/index';
6
+ import HighlightSource from '../src/model/source';
7
+ import { DomNode } from '../src/types';
8
+ import sinon from 'sinon';
9
+ import sources from './fixtures/source.json';
10
+
11
+ describe('Highlighter Hooks', function () {
12
+ this.timeout(50000);
13
+
14
+ let highlighter: Highlighter;
15
+ let cleanup;
16
+
17
+ beforeEach(() => {
18
+ const html = readFileSync(resolve(__dirname, 'fixtures', 'index.html'), 'utf-8');
19
+ cleanup = jsdomGlobal();
20
+ document.body.innerHTML = html;
21
+ highlighter = new Highlighter();
22
+ });
23
+
24
+ describe('#Render.UUID', () => {
25
+ let listener: sinon.SinonSpy<any[], any[]>;
26
+ let id = 'customize-id';
27
+
28
+ beforeEach(() => {
29
+ listener = sinon.spy();
30
+ (highlighter as any).on(Highlighter.event.CREATE, listener);
31
+ });
32
+
33
+ it('should use the customized id', () => {
34
+ highlighter.hooks.Render.UUID.tap(() => id);
35
+
36
+ const range = document.createRange();
37
+ const $p = document.querySelectorAll('p')[0];
38
+ range.setStart($p.childNodes[0], 0);
39
+ range.setEnd($p.childNodes[0], 17);
40
+ highlighter.fromRange(range);
41
+
42
+ expect(listener.args[0][0].sources[0].id).to.be.equal(id);
43
+ });
44
+
45
+ it('should use the internal uuid id when return undefined in the hook', () => {
46
+ const spy: sinon.SinonSpy<any[], string> = sinon.spy();
47
+ highlighter.hooks.Render.UUID.tap(spy);
48
+
49
+ const range = document.createRange();
50
+ const $p = document.querySelectorAll('p')[0];
51
+ range.setStart($p.childNodes[0], 0);
52
+ range.setEnd($p.childNodes[0], 17);
53
+ highlighter.fromRange(range);
54
+
55
+ expect(spy.calledOnce).to.be.true;
56
+ expect(listener.args[0][0].sources[0].id).to.not.equal(id);
57
+ });
58
+
59
+ it('should get correct arguments in the hook', () => {
60
+ const spy: sinon.SinonSpy<any[], string> = sinon.spy();
61
+ highlighter.hooks.Render.UUID.tap(spy);
62
+
63
+ const range = document.createRange();
64
+ const $p = document.querySelectorAll('p')[0];
65
+ range.setStart($p.childNodes[0], 0);
66
+ range.setEnd($p.childNodes[0], 17);
67
+ const content = range.toString();
68
+ highlighter.fromRange(range);
69
+
70
+ expect(spy.args[0][0].$node).to.be.equal($p.childNodes[0]);
71
+ expect(spy.args[0][0].offset).to.be.equal(0);
72
+ expect(spy.args[0][1].$node).to.be.equal($p.childNodes[0]);
73
+ expect(spy.args[0][1].offset).to.be.equal(17);
74
+ expect(spy.args[0][2]).to.be.equal(content);
75
+ });
76
+ });
77
+
78
+ describe('#Render.SelectedNodes', () => {
79
+
80
+ it('should not affect the document when return an empty array in the hook', () => {
81
+ highlighter.hooks.Render.SelectedNodes.tap(() => []);
82
+
83
+ const html = document.body.innerHTML;
84
+ const range = document.createRange();
85
+ const $p = document.querySelectorAll('p')[0];
86
+ range.setStart($p.childNodes[0], 0);
87
+ range.setEnd($p.childNodes[0], 17);
88
+ highlighter.fromRange(range);
89
+
90
+ expect(document.body.innerHTML).to.be.equal(html);
91
+ });
92
+
93
+ it('should not affect the document when return undefined in the hook', () => {
94
+ highlighter.hooks.Render.SelectedNodes.tap(() => []);
95
+
96
+ const html = document.body.innerHTML;
97
+ const range = document.createRange();
98
+ const $p = document.querySelectorAll('p')[0];
99
+ range.setStart($p.childNodes[0], 0);
100
+ range.setEnd($p.childNodes[0], 17);
101
+
102
+ expect(() => highlighter.fromRange(range)).not.to.throw();
103
+ expect(document.body.innerHTML).to.be.equal(html);
104
+ });
105
+
106
+ it('should get correct arguments in the hook', () => {
107
+ const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
108
+ highlighter.hooks.Render.SelectedNodes.tap(spy);
109
+
110
+ const range = document.createRange();
111
+ const $p = document.querySelectorAll('p')[0];
112
+ range.setStart($p.childNodes[0], 0);
113
+ range.setEnd($p.childNodes[0], 17);
114
+ highlighter.fromRange(range);
115
+
116
+ expect(spy.calledOnce).to.be.true;
117
+ expect(spy.args[0][0]).not.to.be.empty;
118
+ expect(spy.args[0][0]).not.to.be.string;
119
+ expect(spy.args[0][1]).lengthOf(1);
120
+ });
121
+ });
122
+
123
+ describe('#Render.WrapNode', () => {
124
+ it('should get correct arguments in the hook', () => {
125
+ const spy: sinon.SinonSpy<any[], HTMLElement> = sinon.spy();
126
+ highlighter.hooks.Render.WrapNode.tap(spy);
127
+
128
+ const range = document.createRange();
129
+ range.setStart(document.querySelectorAll('p')[0].childNodes[0], 0);
130
+ range.setEnd(document.querySelectorAll('p')[0].childNodes[0], 17);
131
+ highlighter.fromRange(range);
132
+
133
+ expect(spy.calledOnce).to.be.true;
134
+ expect(spy.args[0][0]).not.to.be.empty;
135
+ expect(spy.args[0][0]).not.to.be.string;
136
+ expect(Highlighter.isHighlightWrapNode(spy.args[0][1])).to.be.true;
137
+ });
138
+
139
+ it('should be called multiple times when creating multiple wrappers', () => {
140
+ const spy: sinon.SinonSpy<any[], HTMLElement> = sinon.spy();
141
+ highlighter.hooks.Render.WrapNode.tap(spy);
142
+
143
+ const range = document.createRange();
144
+ range.setStart(document.querySelectorAll('p')[0].childNodes[0], 0);
145
+ range.setEnd(document.querySelectorAll('p')[1].childNodes[0], 17);
146
+ highlighter.fromRange(range);
147
+
148
+ expect(spy.callCount).to.be.equal(3);
149
+ });
150
+ });
151
+
152
+ describe('#Serialize.Restore', () => {
153
+ it('should get correct arguments in the hook', () => {
154
+ const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
155
+ highlighter.hooks.Serialize.Restore.tap(spy);
156
+
157
+ const extra = 'this is for testing extra';
158
+ const s = sources[0];
159
+ highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id, extra);
160
+
161
+ expect(spy.calledOnce).to.be.true;
162
+
163
+ const source: HighlightSource = spy.args[0][0];
164
+ expect(source, 'source should not be empty').not.to.be.empty;
165
+ expect(source.extra, 'source extra info should be the same').to.be.equal(extra);
166
+
167
+ const start: DomNode = spy.args[0][1];
168
+ expect(start.$node instanceof Node, 'start .$node to be a Node').to.be.true;
169
+ expect(typeof start.offset, 'start .offset to be a number').to.be.equal('number');
170
+
171
+ const end: DomNode = spy.args[0][2];
172
+ expect(end.$node instanceof Node, 'end .$node to be a Node').to.be.true;
173
+ expect(typeof end.offset, 'end .offset to be a number').to.be.equal('number');
174
+ });
175
+
176
+ it('should change the selection when returning other values in the hook', () => {
177
+ const start = 5;
178
+ const end = 9;
179
+ highlighter.hooks.Serialize.Restore.tap((_: HighlightSource, s: DomNode, e: DomNode) => {
180
+ s.offset = start;
181
+ e.offset = end;
182
+ return [s , e];
183
+ });
184
+
185
+ const s = sources[0];
186
+ highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id);
187
+ const doms = highlighter.getDoms(s.id);
188
+
189
+ expect(doms).lengthOf(1);
190
+ expect(doms[0].textContent).to.be.equal(s.text.slice(start, end));
191
+ });
192
+ });
193
+
194
+ describe('#Serialize.RecordInfo', () => {
195
+ it('should get correct arguments in the hook', () => {
196
+ const spy: sinon.SinonSpy<any[], string> = sinon.spy();
197
+ highlighter.hooks.Serialize.RecordInfo.tap(spy);
198
+
199
+ const range = document.createRange();
200
+ range.setStart(document.querySelectorAll('p')[0].childNodes[0], 0);
201
+ range.setEnd(document.querySelectorAll('p')[0].childNodes[0], 17);
202
+ highlighter.fromRange(range);
203
+
204
+ expect(spy.calledOnce).to.be.true;
205
+ expect(spy.args[0][0]).not.to.be.empty;
206
+ expect(spy.args[0][1]).not.to.be.empty;
207
+ expect(spy.args[0][2]).to.be.equal(document);
208
+ });
209
+
210
+ it('should add extra info to sources', () => {
211
+ const extra = 'test-extra';
212
+ const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
213
+ (highlighter as any).on(Highlighter.event.CREATE, spy);
214
+ highlighter.hooks.Serialize.RecordInfo.tap(() => extra);
215
+
216
+ const range = document.createRange();
217
+ range.setStart(document.querySelectorAll('p')[0].childNodes[0], 0);
218
+ range.setEnd(document.querySelectorAll('p')[0].childNodes[0], 17);
219
+ highlighter.fromRange(range);
220
+
221
+ expect(spy.args[0][0].sources[0].extra).to.be.equal(extra);
222
+ });
223
+ });
224
+
225
+ describe('#Remove.UpdateNodes', () => {
226
+ it('should get correct arguments in the hook', () => {
227
+ const spy: sinon.SinonSpy<any[], any[]> = sinon.spy();
228
+ highlighter.hooks.Remove.UpdateNodes.tap(spy);
229
+
230
+ const s = sources[0];
231
+ highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id);
232
+ highlighter.remove(s.id);
233
+
234
+ expect(spy.calledOnce).to.be.true;
235
+ expect(spy.args[0][0]).to.be.equal(s.id);
236
+ expect(Highlighter.isHighlightWrapNode(spy.args[0][1])).to.be.true;
237
+ expect(spy.args[0][2]).to.be.equal('remove');
238
+ });
239
+ });
240
+
241
+ afterEach(() => {
242
+ cleanup();
243
+ });
244
+ });
@@ -0,0 +1,48 @@
1
+ import { expect } from 'chai';
2
+ import { readFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import jsdomGlobal from 'jsdom-global';
5
+ import Highlighter from '../src/index';
6
+ import HighlightSource from '../src/model/source';
7
+
8
+ describe('Integration Usage', function () {
9
+ this.timeout(50000);
10
+
11
+ let cleanup;
12
+
13
+ beforeEach(() => {
14
+ cleanup = jsdomGlobal();
15
+ });
16
+
17
+ it('should work correctly when root contains no dom', () => {
18
+ const html = readFileSync(resolve(__dirname, 'fixtures', 'index.html'), 'utf-8');
19
+ document.body.innerHTML = html;
20
+
21
+ let highlighter = new Highlighter({
22
+ $root: document.querySelector('p')
23
+ });
24
+
25
+ let s: HighlightSource;
26
+ (highlighter as any).on(Highlighter.event.CREATE, data => {
27
+ s = data.sources[0];
28
+ });
29
+
30
+ const range = document.createRange();
31
+ const $p = document.querySelectorAll('p')[0];
32
+ range.setStart($p.childNodes[0], 0);
33
+ range.setEnd($p.childNodes[0], 17);
34
+ highlighter.fromRange(range);
35
+ const htmlHighlighted = document.body.innerHTML;
36
+
37
+ document.body.innerHTML = html;
38
+ highlighter = new Highlighter({
39
+ $root: document.querySelector('p')
40
+ });
41
+ highlighter.fromStore(s.startMeta, s.endMeta, s.text, s.id);
42
+ expect(document.body.innerHTML).to.be.equal(htmlHighlighted);
43
+ });
44
+
45
+ afterEach(() => {
46
+ cleanup();
47
+ });
48
+ });