nnw-theme 0.0.0 → 1.0.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 (60) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +75 -1
  3. package/THIRD_PARTY_NOTICES.md +14 -0
  4. package/assets/fixtures/article.toml +24 -0
  5. package/assets/fixtures/kitchen-sink.toml +36 -0
  6. package/assets/footnotes.js +174 -0
  7. package/assets/guide/design-checklist.md +19 -0
  8. package/assets/guide/fixtures.md +42 -0
  9. package/assets/guide/publishing.md +34 -0
  10. package/assets/guide/skill.md +44 -0
  11. package/assets/guide/theme-format.md +52 -0
  12. package/assets/netnewswire/LICENSE +21 -0
  13. package/assets/netnewswire/Mac/main_mac.js +43 -0
  14. package/assets/netnewswire/Mac/page.html +12 -0
  15. package/assets/netnewswire/Shared/core.css +186 -0
  16. package/assets/netnewswire/Shared/main.js +221 -0
  17. package/assets/netnewswire/Shared/newsfoot.js +173 -0
  18. package/assets/netnewswire/iOS/main_ios.js +520 -0
  19. package/assets/netnewswire/iOS/page.html +19 -0
  20. package/assets/netnewswire/netnewswire.json +46 -0
  21. package/assets/stubs/.agents/skills/creating-nnw-themes/SKILL.md +22 -0
  22. package/assets/stubs/.agents/skills/creating-nnw-themes/agents/openai.yaml +4 -0
  23. package/assets/stubs/.github/workflows/check.yml +14 -0
  24. package/assets/stubs/.github/workflows/pages.yml +24 -0
  25. package/assets/stubs/.github/workflows/release.yml +24 -0
  26. package/assets/stubs/.github/workflows/screenshot.yml +27 -0
  27. package/assets/stubs/AGENTS.md +24 -0
  28. package/dist/browser.js +227 -0
  29. package/dist/cli.js +3 -0
  30. package/dist/commands/bump.js +23 -0
  31. package/dist/commands/capture.js +26 -0
  32. package/dist/commands/check.js +67 -0
  33. package/dist/commands/completion.js +8 -0
  34. package/dist/commands/guide.js +9 -0
  35. package/dist/commands/init.js +155 -0
  36. package/dist/commands/marketplace.js +22 -0
  37. package/dist/commands/package.js +16 -0
  38. package/dist/commands/preview.js +49 -0
  39. package/dist/commands/progress.js +31 -0
  40. package/dist/commands/release-check.js +45 -0
  41. package/dist/commands/render.js +17 -0
  42. package/dist/commands/screenshot.js +36 -0
  43. package/dist/commands/setup.js +4 -0
  44. package/dist/commands/update.js +7 -0
  45. package/dist/commands.js +175 -0
  46. package/dist/completion.js +203 -0
  47. package/dist/interactive.js +32 -0
  48. package/dist/main.js +229 -0
  49. package/dist/netnewswire.js +84 -0
  50. package/dist/package.js +28 -0
  51. package/dist/plist.js +175 -0
  52. package/dist/project.js +195 -0
  53. package/dist/pyformat.js +82 -0
  54. package/dist/render.js +516 -0
  55. package/dist/stubs.js +49 -0
  56. package/dist/urlparse.js +32 -0
  57. package/dist/validate.js +259 -0
  58. package/dist/zip.js +72 -0
  59. package/lldb/nnwdump.py +151 -0
  60. package/package.json +55 -2
@@ -0,0 +1,173 @@
1
+ // @ts-check
2
+ (function () {
3
+ /** @param {Node | null} el */
4
+ const remove = (el) => { if (el) el.parentElement.removeChild(el) };
5
+
6
+ const stripPx = (s) => +s.slice(0, -2);
7
+
8
+ /** @param {string} tag
9
+ * @param {string} cls
10
+ * @returns HTMLElement
11
+ */
12
+ function newEl(tag, cls) {
13
+ const el = document.createElement(tag);
14
+ el.classList.add(cls);
15
+ return el;
16
+ }
17
+
18
+ /** @type {<T extends any[]>(fn: (...args: T) => void, t: number) => ((...args: T) => void)} */
19
+ function debounce(f, ms) {
20
+ let t = Date.now();
21
+ return (...args) => {
22
+ const now = Date.now();
23
+ if (now - t < ms) return;
24
+ t = now;
25
+ f(...args);
26
+ };
27
+ }
28
+
29
+ const clsPrefix = "newsfoot-footnote-";
30
+ const CONTAINER_CLS = `${clsPrefix}container`;
31
+ const POPOVER_CLS = `${clsPrefix}popover`;
32
+ const POPOVER_INNER_CLS = `${clsPrefix}popover-inner`;
33
+ const POPOVER_ARROW_CLS = `${clsPrefix}popover-arrow`;
34
+
35
+ /**
36
+ * @param {string} content
37
+ * @returns {HTMLElement}
38
+ */
39
+ function footnoteMarkup(content) {
40
+ const popover = newEl("div", POPOVER_CLS);
41
+ const arrow = newEl("div", POPOVER_ARROW_CLS);
42
+ const inner = newEl("div", POPOVER_INNER_CLS);
43
+ popover.appendChild(inner);
44
+ popover.appendChild(arrow);
45
+ inner.innerHTML = content;
46
+ return popover;
47
+ }
48
+
49
+ class Footnote {
50
+ /**
51
+ * @param {string} content
52
+ * @param {Element} fnref
53
+ */
54
+ constructor(content, fnref) {
55
+ this.popover = footnoteMarkup(content);
56
+ this.style = window.getComputedStyle(this.popover);
57
+ this.fnref = fnref;
58
+ let container = this.fnref.closest(`.${CONTAINER_CLS}`);
59
+ container.insertBefore(this.popover, fnref);
60
+ container.style.zIndex = 1;
61
+ /** @type {HTMLElement} */
62
+ this.arrow = this.popover.querySelector(`.${POPOVER_ARROW_CLS}`);
63
+ this.reposition();
64
+
65
+ /** @type {(ev:MouseEvent) => void} */
66
+ this.clickoutHandler = (ev) => {
67
+ if (!(ev.target instanceof Element)) return;
68
+ // Keep this popover open for clicks anywhere inside it, including a nested footnote popover.
69
+ // <https://github.com/Ranchero-Software/NetNewsWire/issues/2372>
70
+ if (this.popover.contains(ev.target)) return;
71
+ if (ev.target === this.fnref) {
72
+ ev.stopPropagation();
73
+ ev.preventDefault();
74
+ }
75
+ this.cleanup();
76
+ }
77
+ document.addEventListener("click", this.clickoutHandler, {capture: true});
78
+
79
+ this.resizeHandler = debounce(() => this.reposition(), 20);
80
+ window.addEventListener("resize", this.resizeHandler);
81
+ }
82
+
83
+ cleanup() {
84
+ this.fnref.closest(`.${CONTAINER_CLS}`).style.zIndex = 0;
85
+ remove(this.popover);
86
+ document.removeEventListener("click", this.clickoutHandler, {capture: true});
87
+ window.removeEventListener("resize", this.resizeHandler);
88
+ delete this.popover;
89
+ delete this.clickoutHandler;
90
+ delete this.resizeHandler;
91
+ }
92
+
93
+ reposition() {
94
+ const refRect = this.fnref.getBoundingClientRect();
95
+ const center = refRect.left + (refRect.width / 2);
96
+ const popoverHalfWidth = this.popover.clientWidth / 2;
97
+ const marginLeft = stripPx(this.style.marginLeft);
98
+ const marginRight = stripPx(this.style.marginRight);
99
+
100
+ const rightOverhang = center + popoverHalfWidth + marginRight > window.innerWidth;
101
+ const leftOverhang = center - (popoverHalfWidth + marginLeft) < 0;
102
+
103
+ let offset = 0;
104
+ if (leftOverhang || rightOverhang) {
105
+ // Popover can't be centered on the ref without overflowing the viewport.
106
+ // Center it horizontally in the viewport so left/right margins are balanced.
107
+ // The arrow gets the inverse transform below and stays anchored to the ref.
108
+ offset = (window.innerWidth / 2) - center;
109
+ }
110
+ this.popover.style.transform = `translate(${offset}px)`;
111
+ this.arrow.style.transform = `translate(${-offset}px) rotate(45deg)`;
112
+ }
113
+ }
114
+
115
+ /** @param {HTMLAnchorElement} a */
116
+ function installContainer(a) {
117
+ if (!a.parentElement.matches(`.${CONTAINER_CLS}`)) {
118
+ const container = newEl("div", CONTAINER_CLS);
119
+ a.parentElement.insertBefore(container, a);
120
+ container.appendChild(a);
121
+ }
122
+ }
123
+
124
+ function idFromHash(target) {
125
+ if (!target.hash) return;
126
+ return decodeURIComponent(target.hash.substring(1));
127
+ }
128
+ /** @type {{fnref(target:HTMLAnchorElement): string|undefined}[]} */
129
+ const footnoteFormats = [
130
+ { // Multimarkdown
131
+ fnref(target) {
132
+ if (!target.matches(".footnote")) return;
133
+ return idFromHash(target);
134
+ }
135
+ }
136
+ ];
137
+
138
+ // Handle clicks on the footnote reference
139
+ document.addEventListener("click", (ev) => {
140
+ if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
141
+
142
+ let targetId = undefined;
143
+ for(const f of footnoteFormats) {
144
+ targetId = f.fnref(ev.target);
145
+ if (targetId) break;
146
+ }
147
+ if (targetId === undefined) return;
148
+
149
+ // Only override the default behaviour when we know we can find the
150
+ // target element
151
+ const targetElement = document.getElementById(targetId);
152
+ if (targetElement === null) return;
153
+
154
+ ev.preventDefault();
155
+
156
+ installContainer(ev.target);
157
+
158
+ void new Footnote(targetElement.innerHTML, ev.target);
159
+ });
160
+
161
+ // Handle clicks on the footnote reverse link
162
+ document.addEventListener("click", (ev) =>
163
+ {
164
+ if (!(ev.target && ev.target instanceof HTMLAnchorElement)) return;
165
+ if (!ev.target.matches(".footnotes .reversefootnote, .footnotes .footnoteBackLink, .footnotes .footnote-return, .footnotes a[href*='#fn'], .footnotes a[href^='#']")) return;
166
+ const id = idFromHash(ev.target);
167
+ if (!id) return;
168
+ const fnref = document.getElementById(id);
169
+
170
+ window.scrollTo({ top: fnref.getBoundingClientRect().top + window.scrollY });
171
+ ev.preventDefault();
172
+ });
173
+ }());
@@ -0,0 +1,520 @@
1
+ var activeImageViewer = null;
2
+
3
+ class ImageViewer {
4
+ constructor(img) {
5
+ this.img = img;
6
+ this.loadingInterval = null;
7
+ this.activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg==";
8
+ }
9
+
10
+ isLoaded() {
11
+ // img.complete covers images that loaded before the onload handler was added.
12
+ return this.img.classList.contains("nnwLoaded") || this.img.complete;
13
+ }
14
+
15
+ clicked() {
16
+ this.showLoadingIndicator();
17
+ if (this.isLoaded()) {
18
+ this.showViewer();
19
+ } else {
20
+ var callback = () => {
21
+ if (this.isLoaded()) {
22
+ clearInterval(this.loadingInterval);
23
+ this.showViewer();
24
+ }
25
+ }
26
+ this.loadingInterval = setInterval(callback, 100);
27
+ }
28
+ }
29
+ cancel() {
30
+ clearInterval(this.loadingInterval);
31
+ this.hideLoadingIndicator();
32
+ }
33
+
34
+ showViewer() {
35
+ this.hideLoadingIndicator();
36
+
37
+ const rect = this.img.getBoundingClientRect();
38
+
39
+ // Instead of trying to convert to canvas (which fails with CORS),
40
+ // send the original image src URL
41
+ const message = {
42
+ x: rect.x,
43
+ y: rect.y,
44
+ width: rect.width,
45
+ height: rect.height,
46
+ imageTitle: this.img.title,
47
+ imageURL: this.img.src,
48
+ };
49
+
50
+ var jsonMessage = JSON.stringify(message);
51
+ window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage);
52
+ }
53
+
54
+ hideImage() {
55
+ this.img.style.opacity = 0;
56
+ }
57
+
58
+ showImage() {
59
+ this.img.style.opacity = 1
60
+ }
61
+
62
+ showLoadingIndicator() {
63
+ var wrapper = document.createElement("div");
64
+ wrapper.classList.add("activityIndicatorWrap");
65
+ this.img.parentNode.insertBefore(wrapper, this.img);
66
+ wrapper.appendChild(this.img);
67
+
68
+ var activityIndicatorImg = document.createElement("img");
69
+ activityIndicatorImg.classList.add("activityIndicator");
70
+ activityIndicatorImg.style.opacity = 0;
71
+ activityIndicatorImg.src = this.activityIndicator;
72
+ wrapper.appendChild(activityIndicatorImg);
73
+
74
+ activityIndicatorImg.style.opacity = 1;
75
+ }
76
+
77
+ hideLoadingIndicator() {
78
+ var wrapper = this.img.parentNode;
79
+ if (wrapper.classList.contains("activityIndicatorWrap")) {
80
+ var wrapperParent = wrapper.parentNode;
81
+ wrapperParent.insertBefore(this.img, wrapper);
82
+ wrapperParent.removeChild(wrapper);
83
+ }
84
+ }
85
+
86
+ static init() {
87
+ cancelImageLoad();
88
+
89
+ // keep track of when an image has finished downloading for ImageViewer
90
+ document.querySelectorAll("img").forEach(element => {
91
+ element.onload = function() {
92
+ this.classList.add("nnwLoaded");
93
+ }
94
+ });
95
+
96
+ // Add the click listener for images
97
+ window.onclick = function(event) {
98
+ if (event.target.matches("img") && !event.target.classList.contains("nnw-nozoom")) {
99
+ // An image inside a link navigates — it might be the only link to an
100
+ // important page. Zoom only standalone images.
101
+ // <https://github.com/Ranchero-Software/NetNewsWire/issues/3641>
102
+ if (event.target.closest("a[href]")) {
103
+ return;
104
+ }
105
+ if (activeImageViewer && activeImageViewer.img === event.target) {
106
+ cancelImageLoad();
107
+ } else {
108
+ cancelImageLoad();
109
+ activeImageViewer = new ImageViewer(event.target);
110
+ activeImageViewer.clicked();
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ function cancelImageLoad() {
118
+ if (activeImageViewer) {
119
+ activeImageViewer.cancel();
120
+ activeImageViewer = null;
121
+ }
122
+ }
123
+
124
+ function hideClickedImage() {
125
+ if (activeImageViewer) {
126
+ activeImageViewer.hideImage();
127
+ }
128
+ }
129
+
130
+ // Used to animate the transition from a fullscreen image
131
+ function showClickedImage() {
132
+ if (activeImageViewer) {
133
+ activeImageViewer.showImage();
134
+ }
135
+ window.webkit.messageHandlers.imageWasShown.postMessage("");
136
+ }
137
+
138
+ function showFeedInspectorSetup() {
139
+ const imageIcon = document.getElementById("nnwImageIcon");
140
+ if (!imageIcon) {
141
+ return;
142
+ }
143
+
144
+ // Tell VoiceOver the icon is a button that opens the Feed Info sheet.
145
+ // <https://github.com/Ranchero-Software/NetNewsWire/issues/4591>
146
+ imageIcon.setAttribute("role", "button");
147
+ imageIcon.setAttribute("tabindex", "0");
148
+ imageIcon.setAttribute("aria-label", nnwGetFeedInfoLabel);
149
+
150
+ imageIcon.onclick = function(event) {
151
+ window.webkit.messageHandlers.showFeedInspector.postMessage("");
152
+ }
153
+ }
154
+
155
+ function postRenderProcessing() {
156
+ ImageViewer.init();
157
+ showFeedInspectorSetup();
158
+ postMediaSourceURLs();
159
+ }
160
+
161
+ // Tell the app which URLs are embedded media, so it can tell a real link tap
162
+ // from WebKit’s synthesized link activation for a video’s source.
163
+ function postMediaSourceURLs() {
164
+ var urls = new Set();
165
+ document.querySelectorAll("video, audio, video source, audio source").forEach(element => {
166
+ if (element.src) {
167
+ urls.add(element.src);
168
+ }
169
+ if (element.currentSrc) {
170
+ urls.add(element.currentSrc);
171
+ }
172
+ });
173
+ window.webkit.messageHandlers.mediaSourceURLs.postMessage(Array.from(urls));
174
+ }
175
+
176
+ function onResize() {
177
+ const meta = document.querySelector("meta[name=viewport]");
178
+
179
+ if (!meta) return;
180
+
181
+ const originalContent = meta.content;
182
+ meta.setAttribute("content", originalContent + ", maximum-scale=1.0");
183
+ meta.setAttribute("content", originalContent);
184
+ }
185
+ window.addEventListener("resize", onResize);
186
+
187
+
188
+ function makeHighlightRect({left, top, width, height}, offsetTop=0, offsetLeft=0) {
189
+ const overlay = document.createElement('a');
190
+
191
+ Object.assign(overlay.style, {
192
+ position: 'absolute',
193
+ left: `${Math.floor(left + offsetLeft)}px`,
194
+ top: `${Math.floor(top + offsetTop)}px`,
195
+ width: `${Math.ceil(width)}px`,
196
+ height: `${Math.ceil(height)}px`,
197
+ backgroundColor: 'rgba(200, 220, 10, 0.4)',
198
+ pointerEvents: 'none'
199
+ });
200
+
201
+ return overlay;
202
+ }
203
+
204
+ function clearHighlightRects() {
205
+ let container = document.getElementById('nnw:highlightContainer')
206
+ if (container) container.remove();
207
+ }
208
+
209
+ function highlightRects(rects, clearOldRects=true, makeHighlightRect=makeHighlightRect) {
210
+ const article = document.querySelector('article');
211
+ let container = document.getElementById('nnw:highlightContainer');
212
+
213
+ article.style.position = 'relative';
214
+
215
+ if (container && clearOldRects)
216
+ container.remove();
217
+
218
+ container = document.createElement('div');
219
+ container.id = 'nnw:highlightContainer';
220
+ article.appendChild(container);
221
+
222
+ const {top, left} = article.getBoundingClientRect();
223
+ return Array.from(rects, rect =>
224
+ container.appendChild(makeHighlightRect(rect, -top, -left))
225
+ );
226
+ }
227
+
228
+ FinderResult = class {
229
+ constructor(result) {
230
+ Object.assign(this, result);
231
+ }
232
+
233
+ range() {
234
+ const range = document.createRange();
235
+ range.setStart(this.node, this.offset);
236
+ range.setEnd(this.node, this.offsetEnd);
237
+ return range;
238
+ }
239
+
240
+ bounds() {
241
+ return this.range().getBoundingClientRect();
242
+ }
243
+
244
+ rects() {
245
+ return this.range().getClientRects();
246
+ }
247
+
248
+ highlight({clearOldRects=true, fn=makeHighlightRect} = {}) {
249
+ highlightRects(this.rects(), clearOldRects, fn);
250
+ }
251
+
252
+ scrollTo() {
253
+ scrollToRect(this.bounds(), this.node);
254
+ }
255
+
256
+ toJSON() {
257
+ return {
258
+ rects: Array.from(this.rects()),
259
+ bounds: this.bounds(),
260
+ index: this.index,
261
+ matchGroups: this.match
262
+ };
263
+ }
264
+
265
+ toJSONString() {
266
+ return JSON.stringify(this.toJSON());
267
+ }
268
+ }
269
+
270
+ Finder = class {
271
+ constructor(pattern, options) {
272
+ if (!pattern.global) {
273
+ pattern = new RegExp(pattern, 'g');
274
+ }
275
+
276
+ this.pattern = pattern;
277
+ this.lastResult = null;
278
+ this._nodeMatches = [];
279
+ this.options = {
280
+ rootSelector: '.articleBody',
281
+ startNode: null,
282
+ startOffset: null,
283
+ }
284
+
285
+ this.resultIndex = -1
286
+
287
+ Object.assign(this.options, options);
288
+
289
+ this.walker = document.createTreeWalker(this.root, NodeFilter.SHOW_TEXT);
290
+ }
291
+
292
+ get root() {
293
+ return document.querySelector(this.options.rootSelector)
294
+ }
295
+
296
+ get count() {
297
+ const node = this.walker.currentNode;
298
+ const index = this.resultIndex;
299
+ this.reset();
300
+
301
+ let result, count = 0;
302
+ while ((result = this.next())) ++count;
303
+
304
+ this.resultIndex = index;
305
+ this.walker.currentNode = node;
306
+
307
+ return count;
308
+ }
309
+
310
+ reset() {
311
+ this.walker.currentNode = this.options.startNode || this.root;
312
+ this.resultIndex = -1;
313
+ }
314
+
315
+ [Symbol.iterator]() {
316
+ return this;
317
+ }
318
+
319
+ next({wrap = false} = {}) {
320
+ const { startNode } = this.options;
321
+ const { pattern, walker } = this;
322
+
323
+ let { node, matchIndex = -1 } = this.lastResult || { node: startNode };
324
+
325
+ while (true) {
326
+ if (!node)
327
+ node = walker.nextNode();
328
+
329
+ if (!node) {
330
+ if (!wrap || this.resultIndex < 0) break;
331
+
332
+ this.reset();
333
+
334
+ continue;
335
+ }
336
+
337
+ let nextIndex = matchIndex + 1;
338
+ let matches = this._nodeMatches;
339
+
340
+ if (!matches.length) {
341
+ matches = Array.from(node.textContent.matchAll(pattern));
342
+ nextIndex = 0;
343
+ }
344
+
345
+ if (matches[nextIndex]) {
346
+ this._nodeMatches = matches;
347
+ const m = matches[nextIndex];
348
+
349
+ this.lastResult = new FinderResult({
350
+ node,
351
+ offset: m.index,
352
+ offsetEnd: m.index + m[0].length,
353
+ text: m[0],
354
+ match: m,
355
+ matchIndex: nextIndex,
356
+ index: ++this.resultIndex,
357
+ });
358
+
359
+ return { value: this.lastResult, done: false };
360
+ }
361
+
362
+ this._nodeMatches = [];
363
+ node = null;
364
+ }
365
+
366
+ return { value: undefined, done: true };
367
+ }
368
+
369
+ /// TODO Call when the search text changes
370
+ retry() {
371
+ if (this.lastResult) {
372
+ this.lastResult.offsetEnd = this.lastResult.offset;
373
+ }
374
+
375
+ }
376
+
377
+ toJSON() {
378
+ const results = Array.from(this);
379
+ }
380
+ }
381
+
382
+ function scrollParent(node) {
383
+ let elt = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
384
+
385
+ while (elt) {
386
+ if (elt.scrollHeight > elt.clientHeight)
387
+ return elt;
388
+ elt = elt.parentElement;
389
+ }
390
+ }
391
+
392
+ function scrollToRect({top, height}, node, pad=20, padBottom=60) {
393
+ const scrollToTop = top - pad;
394
+
395
+ let scrollBy = scrollToTop;
396
+
397
+ if (scrollToTop >= 0) {
398
+ const visible = window.visualViewport;
399
+ const scrollToBottom = top + height + padBottom - visible.height;
400
+ // The top of the rect is already in the viewport
401
+ if (scrollToBottom <= 0 || scrollToTop === 0)
402
+ // Don't need to scroll up--or can't
403
+ return;
404
+
405
+ scrollBy = Math.min(scrollToBottom, scrollBy);
406
+ }
407
+
408
+ scrollParent(node).scrollBy({ top: scrollBy });
409
+ }
410
+
411
+ function withEncodedArg(fn) {
412
+ return function(encodedData, ...rest) {
413
+ const data = encodedData && JSON.parse(atob(encodedData));
414
+ return fn(data, ...rest);
415
+ }
416
+ }
417
+
418
+ function escapeRegex(s) {
419
+ return s.replace(/[.?*+^$\\()[\]{}]/g, '\\$&');
420
+ }
421
+
422
+ class FindState {
423
+ constructor(options) {
424
+ let { text, caseSensitive, regex } = options;
425
+
426
+ if (!regex)
427
+ text = escapeRegex(text);
428
+
429
+ const finder = new Finder(new RegExp(text, caseSensitive ? 'g' : 'ig'));
430
+ this.results = Array.from(finder);
431
+ this.index = -1;
432
+ this.options = options;
433
+ }
434
+
435
+ get selected() {
436
+ return this.index > -1 ? this.results[this.index] : null;
437
+ }
438
+
439
+ toJSON() {
440
+ return {
441
+ index: this.index > -1 ? this.index : null,
442
+ results: this.results,
443
+ count: this.results.length
444
+ };
445
+ }
446
+
447
+ selectNext(step=1) {
448
+ const index = this.index + step;
449
+ const result = this.results[index];
450
+ if (result) {
451
+ this.index = index;
452
+ result.highlight();
453
+ result.scrollTo();
454
+ }
455
+ return result;
456
+ }
457
+
458
+ selectPrevious() {
459
+ return this.selectNext(-1);
460
+ }
461
+ }
462
+
463
+ CurrentFindState = null;
464
+
465
+ const ExcludeKeys = new Set(['top', 'right', 'bottom', 'left']);
466
+ updateFind = withEncodedArg(options => {
467
+ // TODO Start at the current result position
468
+ // TODO Introduce slight delay, cap the number of results, and report results asynchronously
469
+
470
+ let newFindState;
471
+ if (!options || !options.text) {
472
+ clearHighlightRects();
473
+ return
474
+ }
475
+
476
+ try {
477
+ newFindState = new FindState(options);
478
+ } catch (err) {
479
+ clearHighlightRects();
480
+ throw err;
481
+ }
482
+
483
+ if (newFindState.results.length) {
484
+ let selected = CurrentFindState && CurrentFindState.selected;
485
+ let selectIndex = 0;
486
+ if (selected) {
487
+ let {node: currentNode, offset: currentOffset} = selected;
488
+ selectIndex = newFindState.results.findIndex(r => {
489
+ if (r.node === currentNode) {
490
+ return r.offset >= currentOffset;
491
+ }
492
+
493
+ let relation = currentNode.compareDocumentPosition(r.node);
494
+ return Boolean(relation & Node.DOCUMENT_POSITION_FOLLOWING);
495
+ });
496
+ }
497
+
498
+ newFindState.selectNext(selectIndex+1);
499
+ } else {
500
+ clearHighlightRects();
501
+ }
502
+
503
+ CurrentFindState = newFindState;
504
+ return btoa(JSON.stringify(CurrentFindState, (k, v) => (ExcludeKeys.has(k) ? undefined : v)));
505
+ });
506
+
507
+ selectNextResult = withEncodedArg(options => {
508
+ if (CurrentFindState)
509
+ CurrentFindState.selectNext();
510
+ });
511
+
512
+ selectPreviousResult = withEncodedArg(options => {
513
+ if (CurrentFindState)
514
+ CurrentFindState.selectPrevious();
515
+ });
516
+
517
+ function endFind() {
518
+ clearHighlightRects()
519
+ CurrentFindState = null;
520
+ }
@@ -0,0 +1,19 @@
1
+ <html dir="auto">
2
+ <head>
3
+ <title>[[title]]</title>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1">
5
+ <style>
6
+ [[style]]
7
+ </style>
8
+ <script type="text/javascript">
9
+ document.addEventListener("DOMContentLoaded", function(event) {
10
+ window.scrollTo(0, [[windowScrollY]]);
11
+ })
12
+ </script>
13
+ <base href="[[baseURL]]">
14
+ </head>
15
+ <body>
16
+ [[body]]
17
+ </body>
18
+ </html>
19
+