@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,54 @@
1
+ /**
2
+ * all constants
3
+ * cSpell:ignore mengshou
4
+ */
5
+
6
+ import type HighlightSource from '@src/model/source';
7
+ import type { ERROR } from '@src/types';
8
+ import camel from '@src/util/camel';
9
+ import EventEmitter from '@src/util/event.emitter';
10
+
11
+ export const ID_DIVISION = ';';
12
+ export const LOCAL_STORE_KEY = 'highlight-mengshou';
13
+ export const STYLESHEET_ID = 'highlight-mengshou-style';
14
+
15
+ export const DATASET_IDENTIFIER = 'highlight-id';
16
+ export const DATASET_IDENTIFIER_EXTRA = 'highlight-id-extra';
17
+ export const DATASET_SPLIT_TYPE = 'highlight-split-type';
18
+ export const CAMEL_DATASET_IDENTIFIER = camel(DATASET_IDENTIFIER);
19
+ export const CAMEL_DATASET_IDENTIFIER_EXTRA = camel(DATASET_IDENTIFIER_EXTRA);
20
+ export const CAMEL_DATASET_SPLIT_TYPE = camel(DATASET_SPLIT_TYPE);
21
+
22
+ const DEFAULT_WRAP_TAG = 'span';
23
+
24
+ export const getDefaultOptions = () => ({
25
+ $root: document || document.documentElement,
26
+ exceptSelectors: null,
27
+ wrapTag: DEFAULT_WRAP_TAG,
28
+ verbose: false,
29
+ style: {
30
+ className: 'highlight-mengshou-wrap',
31
+ },
32
+ });
33
+
34
+ export const getStylesheet = () => `
35
+ .${getDefaultOptions().style.className} {
36
+ background: #ff9;
37
+ cursor: pointer;
38
+ }
39
+ .${getDefaultOptions().style.className}.active {
40
+ background: #ffb;
41
+ }
42
+ `;
43
+
44
+ export const ROOT_IDX = -2;
45
+ export const UNKNOWN_IDX = -1;
46
+ export const INTERNAL_ERROR_EVENT = 'error';
47
+
48
+ interface EventHandlerMap {
49
+ [key: string]: (...args: any[]) => void;
50
+ error: (data: { type: ERROR; detail?: HighlightSource; error?: any }) => void;
51
+ }
52
+ class ErrorEventEmitter extends EventEmitter<EventHandlerMap> {}
53
+
54
+ export const eventEmitter = new ErrorEventEmitter();
@@ -0,0 +1,37 @@
1
+ interface Deferred<T> {
2
+ promise: Promise<T>;
3
+ resolve: (args: T) => unknown;
4
+ reject: (e?: unknown) => unknown;
5
+ }
6
+
7
+ export default function getDeferred<T>(): Deferred<T> {
8
+ let resolve: (args: T) => unknown;
9
+ let reject: (e?: unknown) => unknown;
10
+
11
+ const promise = new Promise<T>((r, j) => {
12
+ resolve = r;
13
+ reject = j;
14
+ });
15
+
16
+ return {
17
+ promise,
18
+ resolve,
19
+ reject,
20
+ };
21
+ }
22
+
23
+ export const resolve = <T>(data: T) => {
24
+ const defer = getDeferred<T>();
25
+
26
+ defer.resolve(data);
27
+
28
+ return defer.promise;
29
+ };
30
+
31
+ export const reject = <T>(data: T) => {
32
+ const defer = getDeferred<T>();
33
+
34
+ defer.reject(data);
35
+
36
+ return defer.promise;
37
+ };
@@ -0,0 +1,155 @@
1
+ import type { RootElement } from '@src/types';
2
+ import {
3
+ ID_DIVISION,
4
+ DATASET_IDENTIFIER,
5
+ CAMEL_DATASET_IDENTIFIER,
6
+ CAMEL_DATASET_IDENTIFIER_EXTRA,
7
+ } from '@src/util/const';
8
+
9
+ /**
10
+ * whether a wrapper node
11
+ */
12
+ export const isHighlightWrapNode = ($node: HTMLElement): boolean =>
13
+ !!$node.dataset && !!$node.dataset[CAMEL_DATASET_IDENTIFIER];
14
+
15
+ /**
16
+ * ===================================================================================
17
+ * below methods (getHighlightId/getExtraHighlightId)
18
+ * will check whether the node is inside a wrapper iteratively util reach the root node
19
+ * if the node is not inside the root, the id must be empty
20
+ * ====================================================================================
21
+ */
22
+
23
+ const findAncestorWrapperInRoot = ($node: HTMLElement, $root: RootElement): HTMLElement => {
24
+ let isInsideRoot = false;
25
+ let $wrapper: HTMLElement = null;
26
+
27
+ while ($node) {
28
+ if (isHighlightWrapNode($node)) {
29
+ $wrapper = $node;
30
+ }
31
+
32
+ if ($node === $root) {
33
+ isInsideRoot = true;
34
+ break;
35
+ }
36
+
37
+ $node = $node.parentNode as HTMLElement;
38
+ }
39
+
40
+ return isInsideRoot ? $wrapper : null;
41
+ };
42
+
43
+ /**
44
+ * get highlight id by a node
45
+ */
46
+ export const getHighlightId = ($node: HTMLElement, $root: RootElement): string => {
47
+ $node = findAncestorWrapperInRoot($node, $root);
48
+
49
+ if (!$node) {
50
+ return '';
51
+ }
52
+
53
+ return $node.dataset[CAMEL_DATASET_IDENTIFIER];
54
+ };
55
+
56
+ /**
57
+ * get extra highlight id by a node
58
+ */
59
+ export const getExtraHighlightId = ($node: HTMLElement, $root: RootElement): string[] => {
60
+ $node = findAncestorWrapperInRoot($node, $root);
61
+
62
+ if (!$node) {
63
+ return [];
64
+ }
65
+
66
+ return $node.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA].split(ID_DIVISION).filter(i => i);
67
+ };
68
+
69
+ /**
70
+ * get all highlight wrapping nodes nodes from a root node
71
+ */
72
+ export const getHighlightsByRoot = ($roots: RootElement | RootElement[], wrapTag: string): HTMLElement[] => {
73
+ if (!Array.isArray($roots)) {
74
+ $roots = [$roots];
75
+ }
76
+
77
+ const $wraps: HTMLElement[] = [];
78
+
79
+ for (const $r of $roots) {
80
+ const $list = $r.querySelectorAll<HTMLElement>(`${wrapTag}[data-${DATASET_IDENTIFIER}]`);
81
+
82
+ // eslint-disable-next-line prefer-spread
83
+ $wraps.push.apply($wraps, $list);
84
+ }
85
+
86
+ return $wraps;
87
+ };
88
+
89
+ /**
90
+ * get all highlight wrapping nodes by highlight id from a root node
91
+ */
92
+ export const getHighlightById = ($root: RootElement, id: string, wrapTag: string): HTMLElement[] => {
93
+ const $highlights: HTMLElement[] = [];
94
+ const reg = new RegExp(`(${id}\\${ID_DIVISION}|\\${ID_DIVISION}?${id}$)`);
95
+ const $list = $root.querySelectorAll<HTMLElement>(`${wrapTag}[data-${DATASET_IDENTIFIER}]`);
96
+
97
+ for (const $l of $list) {
98
+ const $n = $l;
99
+ const nid = $n.dataset[CAMEL_DATASET_IDENTIFIER];
100
+
101
+ if (nid === id) {
102
+ $highlights.push($n);
103
+ continue;
104
+ }
105
+
106
+ const extraId = $n.dataset[CAMEL_DATASET_IDENTIFIER_EXTRA];
107
+
108
+ if (reg.test(extraId)) {
109
+ $highlights.push($n);
110
+ continue;
111
+ }
112
+ }
113
+
114
+ return $highlights;
115
+ };
116
+
117
+ export const forEach = ($nodes: NodeList, cb: (n: Node, idx: number, s: NodeList) => void): void => {
118
+ for (let i = 0; i < $nodes.length; i++) {
119
+ cb($nodes[i], i, $nodes);
120
+ }
121
+ };
122
+
123
+ export const removeEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject) => {
124
+ $el.removeEventListener(evt, fn);
125
+ };
126
+
127
+ /**
128
+ * maybe be need some polyfill methods later
129
+ * provide unified dom methods for compatibility
130
+ */
131
+ export const addEventListener = ($el: RootElement, evt: string, fn: EventListenerOrEventListenerObject) => {
132
+ $el.addEventListener(evt, fn);
133
+
134
+ return () => {
135
+ removeEventListener($el, evt, fn);
136
+ };
137
+ };
138
+
139
+ export const addClass = ($el: HTMLElement, className: string[] | string) => {
140
+ if (!Array.isArray(className)) {
141
+ className = [className];
142
+ }
143
+
144
+ $el.classList.add(...className);
145
+ };
146
+
147
+ export const removeClass = ($el: HTMLElement, className: string): void => {
148
+ $el.classList.remove(className);
149
+ };
150
+
151
+ export const removeAllClass = ($el: HTMLElement): void => {
152
+ $el.className = '';
153
+ };
154
+
155
+ export const hasClass = ($el: HTMLElement, className: string): boolean => $el.classList.contains(className);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * tiny event emitter
3
+ * modify from mitt
4
+ */
5
+
6
+ type EventHandler = (...data: unknown[]) => void;
7
+
8
+ type EventMap = Record<string, EventHandler>;
9
+ type HandlersMap<T extends EventMap> = {
10
+ [K in keyof T]: T[K][];
11
+ };
12
+
13
+ class EventEmitter<U extends EventMap = EventMap> {
14
+ private handlersMap: HandlersMap<U> = Object.create(null);
15
+
16
+ on<T extends keyof U>(type: T, handler: U[T]) {
17
+ if (!this.handlersMap[type]) {
18
+ this.handlersMap[type] = [];
19
+ }
20
+
21
+ this.handlersMap[type].push(handler);
22
+
23
+ return this;
24
+ }
25
+
26
+ off<T extends keyof U>(type: T, handler: U[T]) {
27
+ if (this.handlersMap[type]) {
28
+ this.handlersMap[type].splice(this.handlersMap[type].indexOf(handler) >>> 0, 1);
29
+ }
30
+
31
+ return this;
32
+ }
33
+
34
+ emit<T extends keyof U>(type: T, ...data: Parameters<U[T]>) {
35
+ if (this.handlersMap[type]) {
36
+ this.handlersMap[type].slice().forEach(handler => {
37
+ handler(...data);
38
+ });
39
+ }
40
+
41
+ return this;
42
+ }
43
+ }
44
+
45
+ export default EventEmitter;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * simple hook
3
+ * webpack-plugin-liked api
4
+ */
5
+
6
+ type HookCallback<T> = (...args: unknown[]) => T;
7
+
8
+ class Hook<T = unknown> {
9
+ name = '';
10
+
11
+ private readonly ops: HookCallback<T>[] = [];
12
+
13
+ constructor(name?) {
14
+ this.name = name;
15
+ }
16
+
17
+ tap(cb: HookCallback<T>) {
18
+ if (this.ops.indexOf(cb) === -1) {
19
+ this.ops.push(cb);
20
+ }
21
+
22
+ return () => {
23
+ this.remove(cb);
24
+ };
25
+ }
26
+
27
+ remove(cb: HookCallback<T>) {
28
+ const idx = this.ops.indexOf(cb);
29
+
30
+ if (idx < 0) {
31
+ return;
32
+ }
33
+
34
+ this.ops.splice(idx, 1);
35
+ }
36
+
37
+ isEmpty() {
38
+ return this.ops.length === 0;
39
+ }
40
+
41
+ call(...args: unknown[]) {
42
+ let ret: T;
43
+
44
+ this.ops.forEach(op => {
45
+ ret = op(...args);
46
+ });
47
+
48
+ return ret;
49
+ }
50
+ }
51
+
52
+ export default Hook;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * adapter for mobile and desktop events
3
+ */
4
+
5
+ import type { IInteraction } from '@src/types';
6
+ import { UserInputEvent } from '@src/types';
7
+ import detectMobile from '@src/util/is.mobile';
8
+
9
+ export default (): IInteraction => {
10
+ const isMobile = detectMobile(window.navigator.userAgent);
11
+
12
+ const interaction: IInteraction = {
13
+ PointerEnd: isMobile ? UserInputEvent.touchend : UserInputEvent.mouseup,
14
+ PointerTap: isMobile ? UserInputEvent.touchstart : UserInputEvent.click,
15
+ // hover and click will be the same event in mobile
16
+ PointerOver: isMobile ? UserInputEvent.touchstart : UserInputEvent.mouseover,
17
+ };
18
+
19
+ return interaction;
20
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * is mobile device?
3
+ */
4
+
5
+ const regMobile = /Android|iPhone|BlackBerry|BB10|Opera Mini|Phone|Mobile|Silk|Windows Phone|Mobile(?:.+)Firefox\b/i;
6
+
7
+ export default (userAgent: string) => regMobile.test(userAgent);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * support IE 10
3
+ */
4
+ export const unique = <T>(arr: T[]): T[] => {
5
+ const res: T[] = [];
6
+
7
+ for (const el of arr) {
8
+ if (res.indexOf(el) === -1) {
9
+ res.push(el);
10
+ }
11
+ }
12
+
13
+ return res;
14
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * generate UUID
3
+ */
4
+
5
+ /* eslint-disable @typescript-eslint/restrict-plus-operands */
6
+ export default function createUUID(a?): string {
7
+ return a
8
+ ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
9
+ : (([1e7] as unknown as string) + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, createUUID);
10
+ }