@adobe/spacecat-shared-html-analyzer 1.0.7 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [@adobe/spacecat-shared-html-analyzer-v1.2.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-html-analyzer-v1.1.0...@adobe/spacecat-shared-html-analyzer-v1.2.0) (2025-12-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * added utility method to get added blocks in markdown diff, made compatible for nodejs scripts ([#1213](https://github.com/adobe/spacecat-shared/issues/1213)) ([07d8b74](https://github.com/adobe/spacecat-shared/commit/07d8b7419a681f2c5e07870c526ae26d28108989))
7
+
8
+ # [@adobe/spacecat-shared-html-analyzer-v1.1.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-html-analyzer-v1.0.7...@adobe/spacecat-shared-html-analyzer-v1.1.0) (2025-12-01)
9
+
10
+
11
+ ### Features
12
+
13
+ * added utilities for markdown diff & conversion from LLMO chrome extension ([#1184](https://github.com/adobe/spacecat-shared/issues/1184)) ([dc9867e](https://github.com/adobe/spacecat-shared/commit/dc9867ea4ac0cf9f8bd2fdc3f22ab74cd3e1f12e))
14
+
1
15
  # [@adobe/spacecat-shared-html-analyzer-v1.0.7](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-html-analyzer-v1.0.6...@adobe/spacecat-shared-html-analyzer-v1.0.7) (2025-11-28)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/spacecat-shared-html-analyzer",
3
- "version": "1.0.7",
3
+ "version": "1.2.0",
4
4
  "description": "Analyze HTML content visibility for AI crawlers and citations - compare static HTML vs fully rendered content",
5
5
  "type": "module",
6
6
  "engines": {
@@ -12,6 +12,7 @@
12
12
  "scripts": {
13
13
  "test": "c8 mocha",
14
14
  "lint": "eslint .",
15
+ "lint:fix": "eslint --fix .",
15
16
  "clean": "rm -rf package-lock.json node_modules",
16
17
  "build": "rollup -c",
17
18
  "build:chrome": "rollup -c && echo '✅ Chrome extension bundle ready: dist/html-analyzer.min.js'"
@@ -36,7 +37,9 @@
36
37
  "access": "public"
37
38
  },
38
39
  "dependencies": {
39
- "cheerio": "^1.0.0-rc.12"
40
+ "cheerio": "^1.0.0-rc.12",
41
+ "turndown": "^7.2.0",
42
+ "marked": "^16.2.0"
40
43
  },
41
44
  "devDependencies": {
42
45
  "@rollup/plugin-node-resolve": "^16.0.1",
package/rollup.config.js CHANGED
@@ -57,8 +57,10 @@ export default {
57
57
  }),
58
58
  ],
59
59
  external: [
60
- // Exclude cheerio from bundle - it won't work in browser anyway
60
+ // Exclude Node.js-only dependencies from bundle - they won't work in browser anyway
61
61
  'cheerio',
62
+ 'turndown',
63
+ 'marked',
62
64
  ],
63
65
  onwarn(warning, warn) {
64
66
  // Suppress warnings about dynamic imports that we'll handle
@@ -32,10 +32,18 @@ import {
32
32
  countLines,
33
33
  diffTokens,
34
34
  generateDiffReport,
35
+ htmlToMarkdown,
36
+ markdownToHtml,
37
+ htmlToMarkdownToHtml,
38
+ diffDOMBlocks,
39
+ createMarkdownTableDiff,
40
+ generateMarkdownDiff,
41
+ htmlToRenderedMarkdown,
35
42
  hashDJB2,
36
43
  pct,
37
44
  formatNumberToK,
38
45
  isBrowser,
46
+ getGlobalObject,
39
47
  } from './index.js';
40
48
 
41
49
  // Create global object for Chrome extension
@@ -60,6 +68,17 @@ const HTMLAnalyzer = {
60
68
  diffTokens,
61
69
  generateDiffReport,
62
70
 
71
+ // Markdown conversion functions
72
+ htmlToMarkdown,
73
+ markdownToHtml,
74
+ htmlToMarkdownToHtml,
75
+
76
+ // Markdown diff functions
77
+ diffDOMBlocks,
78
+ createMarkdownTableDiff,
79
+ generateMarkdownDiff,
80
+ htmlToRenderedMarkdown,
81
+
63
82
  // Utility functions
64
83
  hashDJB2,
65
84
  pct,
@@ -73,17 +92,8 @@ const HTMLAnalyzer = {
73
92
 
74
93
  // Make available globally for Chrome extension script tags
75
94
  // This needs to be executed immediately when the bundle loads
76
- /* eslint-env browser */
77
- /* global window, self */
78
95
  (function setGlobal() {
79
- // Determine the global object (works in browser, Node.js, Web Workers)
80
- const globalObject = (function getGlobalObject() {
81
- if (typeof window !== 'undefined') return window;
82
- if (typeof globalThis !== 'undefined') return globalThis;
83
- if (typeof self !== 'undefined') return self;
84
- return this || {};
85
- }());
86
-
96
+ const globalObject = getGlobalObject();
87
97
  // Assign to global scope
88
98
  globalObject.HTMLAnalyzer = HTMLAnalyzer;
89
99
  }());
package/src/index.js CHANGED
@@ -40,9 +40,24 @@ export {
40
40
  calculateBothScenarioStats,
41
41
  } from './analyzer.js';
42
42
 
43
+ export {
44
+ htmlToMarkdown,
45
+ markdownToHtml,
46
+ htmlToMarkdownToHtml,
47
+ } from './markdown-converter.js';
48
+
49
+ export {
50
+ diffDOMBlocks,
51
+ createMarkdownTableDiff,
52
+ getAddedMarkdownBlocks,
53
+ generateMarkdownDiff,
54
+ htmlToRenderedMarkdown,
55
+ } from './markdown-diff.js';
56
+
43
57
  export {
44
58
  hashDJB2,
45
59
  pct,
46
60
  formatNumberToK,
47
61
  isBrowser,
62
+ getGlobalObject,
48
63
  } from './utils.js';
@@ -0,0 +1,105 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Markdown conversion utilities
15
+ * Provides HTML to Markdown and Markdown to HTML conversions
16
+ */
17
+
18
+ import { isBrowser, getGlobalObject } from './utils.js';
19
+
20
+ // Cache for imported modules in Node.js
21
+ let TurndownServiceClass = null;
22
+ let markedParser = null;
23
+
24
+ /**
25
+ * Get Turndown service instance
26
+ * @private
27
+ * @returns {Promise<Object>} TurndownService instance
28
+ */
29
+ async function getTurndownService() {
30
+ if (isBrowser()) {
31
+ // In browser environment, expect global TurndownService
32
+ const globalObj = getGlobalObject();
33
+ if (globalObj.TurndownService) {
34
+ return new globalObj.TurndownService();
35
+ }
36
+ throw new Error('TurndownService must be loaded in browser environment');
37
+ }
38
+ // In Node.js environment, dynamically import turndown
39
+ if (!TurndownServiceClass) {
40
+ const module = await import('turndown');
41
+ TurndownServiceClass = module.default;
42
+ }
43
+ return new TurndownServiceClass();
44
+ }
45
+
46
+ /**
47
+ * Get marked parser
48
+ * @private
49
+ * @returns {Promise<Object>} marked parser
50
+ */
51
+ async function getMarked() {
52
+ if (isBrowser()) {
53
+ // In browser environment, expect global marked
54
+ const globalObj = getGlobalObject();
55
+ if (globalObj.marked) {
56
+ return globalObj.marked;
57
+ }
58
+ throw new Error('marked must be loaded in browser environment');
59
+ }
60
+ // In Node.js environment, dynamically import marked
61
+ if (!markedParser) {
62
+ const module = await import('marked');
63
+ markedParser = module.marked;
64
+ }
65
+ return markedParser;
66
+ }
67
+
68
+ /**
69
+ * Convert HTML to Markdown
70
+ * @param {string} html - HTML content to convert
71
+ * @returns {Promise<string>} Markdown content
72
+ */
73
+ export async function htmlToMarkdown(html) {
74
+ if (!html || typeof html !== 'string') {
75
+ return '';
76
+ }
77
+
78
+ const turndownService = await getTurndownService();
79
+ return turndownService.turndown(html);
80
+ }
81
+
82
+ /**
83
+ * Convert Markdown to HTML
84
+ * @param {string} markdown - Markdown content to convert
85
+ * @returns {Promise<string>} HTML content
86
+ */
87
+ export async function markdownToHtml(markdown) {
88
+ if (!markdown || typeof markdown !== 'string') {
89
+ return '';
90
+ }
91
+
92
+ const marked = await getMarked();
93
+ return marked.parse(markdown);
94
+ }
95
+
96
+ /**
97
+ * Convert HTML to Markdown and then render it back to HTML
98
+ * Useful for normalizing HTML through markdown representation
99
+ * @param {string} html - HTML content to convert
100
+ * @returns {Promise<string>} Rendered HTML from markdown
101
+ */
102
+ export async function htmlToMarkdownToHtml(html) {
103
+ const markdown = await htmlToMarkdown(html);
104
+ return markdownToHtml(markdown);
105
+ }
@@ -0,0 +1,352 @@
1
+ /*
2
+ * Copyright 2025 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ /**
14
+ * Markdown diff utilities
15
+ * Provides DOM block-level diffing for markdown content
16
+ */
17
+
18
+ import { filterHtmlContent } from './html-filter.js';
19
+ import { htmlToMarkdown, markdownToHtml } from './markdown-converter.js';
20
+
21
+ /**
22
+ * Check if element is a browser DOM element (has outerHTML property)
23
+ * @param {Object} el - Element to check
24
+ * @returns {boolean} True if DOM element, false if cheerio element
25
+ * @private
26
+ */
27
+ function isDOMElement(el) {
28
+ return typeof el.outerHTML === 'string';
29
+ }
30
+
31
+ /**
32
+ * Diff DOM blocks using LCS algorithm
33
+ * Compares blocks based on text content while preserving full HTML structure
34
+ * @param {Array<{html: string, text: string, tagName: string}>} originalBlocks
35
+ * - Original DOM blocks
36
+ * @param {Array<{html: string, text: string, tagName: string}>} currentBlocks
37
+ * - Current DOM blocks
38
+ * @returns {Array<{type: 'same'|'del'|'add', originalBlock?: Object,
39
+ * currentBlock?: Object}>} Diff operations
40
+ */
41
+ export function diffDOMBlocks(originalBlocks, currentBlocks) {
42
+ // Create a mapping function that uses text content for comparison
43
+ // while preserving the full HTML structure
44
+ const A = originalBlocks.map((block) => block.text);
45
+ const B = currentBlocks.map((block) => block.text);
46
+
47
+ // Map tokens to ints for faster LCS
48
+ const sym = new Map();
49
+ const mapTok = (t) => {
50
+ if (!sym.has(t)) sym.set(t, sym.size + 1);
51
+ return sym.get(t);
52
+ };
53
+ const a = A.map(mapTok);
54
+ const b = B.map(mapTok);
55
+
56
+ // LCS length table
57
+ const m = a.length;
58
+ const n = b.length;
59
+ const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
60
+ for (let i = 1; i <= m; i += 1) {
61
+ for (let j = 1; j <= n; j += 1) {
62
+ dp[i][j] = (a[i - 1] === b[j - 1])
63
+ ? dp[i - 1][j - 1] + 1
64
+ : Math.max(dp[i - 1][j], dp[i][j - 1]);
65
+ }
66
+ }
67
+
68
+ // Backtrack to collect ops with full block data
69
+ const ops = [];
70
+ let i = m;
71
+ let j = n;
72
+ while (i > 0 && j > 0) {
73
+ if (a[i - 1] === b[j - 1]) {
74
+ ops.push({
75
+ type: 'same',
76
+ originalBlock: originalBlocks[i - 1],
77
+ currentBlock: currentBlocks[j - 1],
78
+ });
79
+ i -= 1;
80
+ j -= 1;
81
+ } else if (dp[i - 1][j] >= dp[i][j - 1]) {
82
+ ops.push({
83
+ type: 'del',
84
+ originalBlock: originalBlocks[i - 1],
85
+ });
86
+ i -= 1;
87
+ } else {
88
+ ops.push({
89
+ type: 'add',
90
+ currentBlock: currentBlocks[j - 1],
91
+ });
92
+ j -= 1;
93
+ }
94
+ }
95
+ while (i > 0) {
96
+ ops.push({
97
+ type: 'del',
98
+ originalBlock: originalBlocks[i - 1],
99
+ });
100
+ i -= 1;
101
+ }
102
+ while (j > 0) {
103
+ ops.push({
104
+ type: 'add',
105
+ currentBlock: currentBlocks[j - 1],
106
+ });
107
+ j -= 1;
108
+ }
109
+ ops.reverse();
110
+ return ops;
111
+ }
112
+
113
+ /**
114
+ * Get tag name from element
115
+ * @param {Object} el - Element (DOM or cheerio)
116
+ * @returns {string} Uppercase tag name
117
+ * @private
118
+ */
119
+ function getTagName(el) {
120
+ // DOM elements have uppercase tagName, cheerio has lowercase name
121
+ return (el.tagName || el.name || '').toUpperCase();
122
+ }
123
+
124
+ /**
125
+ * Get children elements
126
+ * @param {Object} el - Element (DOM or cheerio)
127
+ * @returns {Array} Array of child elements
128
+ * @private
129
+ */
130
+ function getChildren(el) {
131
+ if (isDOMElement(el)) {
132
+ return Array.from(el.children || []);
133
+ }
134
+ // Cheerio raw element has children array with type info
135
+ return (el.children || []).filter((c) => c.type === 'tag');
136
+ }
137
+
138
+ /**
139
+ * Get text content from element
140
+ * @param {Object} el - Element (DOM or cheerio)
141
+ * @param {Function} [$] - Cheerio instance (required for cheerio elements)
142
+ * @returns {string} Text content
143
+ * @private
144
+ */
145
+ function getTextContent(el, $) {
146
+ if (isDOMElement(el)) {
147
+ return el.textContent || '';
148
+ }
149
+ // Use cheerio's text() method
150
+ return $(el).text();
151
+ }
152
+
153
+ /**
154
+ * Get outer HTML from element
155
+ * @param {Object} el - Element (DOM or cheerio)
156
+ * @param {Function} [$] - Cheerio instance (required for cheerio elements)
157
+ * @returns {string} Outer HTML
158
+ * @private
159
+ */
160
+ function getOuterHTML(el, $) {
161
+ if (isDOMElement(el)) {
162
+ return el.outerHTML;
163
+ }
164
+ // Use cheerio's html() method
165
+ return $.html(el);
166
+ }
167
+
168
+ /**
169
+ * Extract blocks from parsed HTML, breaking down lists into individual items
170
+ * Works with both browser DOM elements and cheerio raw elements
171
+ * @param {Array} children - Array of child elements (DOM or cheerio)
172
+ * @param {Function} [$] - Cheerio instance (required for Node.js, optional for browser)
173
+ * @returns {Array<{html: string, text: string, tagName: string}>} Extracted blocks
174
+ * @private
175
+ */
176
+ function extractBlocks(children, $) {
177
+ const blocks = [];
178
+ children.forEach((el) => {
179
+ const tagName = getTagName(el);
180
+
181
+ // If it's a list (ul/ol), break it down into individual list items
182
+ if (tagName === 'UL' || tagName === 'OL') {
183
+ const listType = tagName.toLowerCase();
184
+ const listChildren = getChildren(el);
185
+
186
+ listChildren.forEach((li) => {
187
+ if (getTagName(li) === 'LI') {
188
+ // Skip empty list items - they cause alignment issues
189
+ const liText = getTextContent(li, $).trim();
190
+ if (!liText) return;
191
+
192
+ // Check if the list item contains nested block elements (p, div, h1-h6, etc.)
193
+ const liChildren = getChildren(li);
194
+ const nestedBlocks = liChildren.filter((child) => {
195
+ const childTag = getTagName(child);
196
+ return childTag === 'P' || childTag === 'DIV' || childTag === 'H1'
197
+ || childTag === 'H2' || childTag === 'H3' || childTag === 'H4'
198
+ || childTag === 'H5' || childTag === 'H6'
199
+ || childTag === 'BLOCKQUOTE' || childTag === 'PRE';
200
+ });
201
+
202
+ if (nestedBlocks.length > 0) {
203
+ // Extract nested blocks individually for better matching
204
+ // but wrap them in li/ul for proper display
205
+ nestedBlocks.forEach((child) => {
206
+ const childText = getTextContent(child, $).trim();
207
+ if (!childText) return; // Skip empty nested blocks too
208
+
209
+ blocks.push({
210
+ html: `<${listType}><li>${getOuterHTML(child, $)}</li></${listType}>`,
211
+ text: childText,
212
+ tagName: getTagName(child).toLowerCase(),
213
+ });
214
+ });
215
+ } else {
216
+ // No nested blocks, treat the whole li as one block
217
+ // wrap in ul/ol for proper display
218
+ blocks.push({
219
+ html: `<${listType}>${getOuterHTML(li, $)}</${listType}>`,
220
+ text: liText,
221
+ tagName: 'li',
222
+ });
223
+ }
224
+ }
225
+ });
226
+ } else {
227
+ // For all other elements, add them as-is
228
+ const text = getTextContent(el, $).trim();
229
+ if (text) {
230
+ blocks.push({
231
+ html: getOuterHTML(el, $),
232
+ text,
233
+ tagName: tagName.toLowerCase(),
234
+ });
235
+ }
236
+ }
237
+ });
238
+ return blocks;
239
+ }
240
+
241
+ /**
242
+ * Get only the added markdown blocks (content in current but not in original)
243
+ * @param {Array} originalChildren - Array of original DOM child elements
244
+ * @param {Array} currentChildren - Array of current DOM child elements
245
+ * @param {Function} [$] - Cheerio instance (required for Node.js, optional for browser)
246
+ * @returns {{addedBlocks: Array<{html: string, text: string}>, addedCount: number}}
247
+ * Added blocks with both HTML and text content
248
+ */
249
+ export function getAddedMarkdownBlocks(originalChildren, currentChildren, $) {
250
+ const originalBlocks = extractBlocks(originalChildren, $);
251
+ const currentBlocks = extractBlocks(currentChildren, $);
252
+
253
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
254
+
255
+ // Extract both HTML and text content from added blocks
256
+ const addedBlocks = ops
257
+ .filter((op) => op.type === 'add')
258
+ .map((op) => ({
259
+ html: op.currentBlock.html,
260
+ text: op.currentBlock.text,
261
+ }));
262
+
263
+ return {
264
+ addedBlocks,
265
+ addedCount: addedBlocks.length,
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Create markdown table diff from parsed DOM children
271
+ * @param {Array} originalChildren - Array of original DOM child elements
272
+ * @param {Array} currentChildren - Array of current DOM child elements
273
+ * @param {Function} [$] - Cheerio instance (required for Node.js, optional for browser)
274
+ * @returns {{tableHtml: string, counters: string}} Diff table and counter information
275
+ */
276
+ export function createMarkdownTableDiff(originalChildren, currentChildren, $) {
277
+ // Get all block-level elements from both sides and extract their text content
278
+ const originalBlocks = extractBlocks(originalChildren, $);
279
+ const currentBlocks = extractBlocks(currentChildren, $);
280
+
281
+ // Run diff algorithm once and count changes
282
+ const ops = diffDOMBlocks(originalBlocks, currentBlocks);
283
+ let addCount = 0;
284
+ let delCount = 0;
285
+
286
+ // Create table rows based on diff operations and count changes
287
+ const tableRows = [];
288
+ ops.forEach((op) => {
289
+ if (op.type === 'same') {
290
+ // Show unchanged blocks on both sides
291
+ const leftContent = op.originalBlock.html;
292
+ const rightContent = op.currentBlock.html;
293
+ tableRows.push(`<tr><td class="diff-line-same markdown-rendered">${leftContent}</td><td class="diff-line-same markdown-rendered">${rightContent}</td></tr>`);
294
+ } else if (op.type === 'del') {
295
+ // Show deleted blocks only on left side
296
+ delCount += 1;
297
+ const leftContent = op.originalBlock.html;
298
+ tableRows.push(`<tr><td class="diff-line-del markdown-rendered">${leftContent}</td><td class="diff-line-empty"></td></tr>`);
299
+ } else if (op.type === 'add') {
300
+ // Show added blocks only on right side
301
+ addCount += 1;
302
+ const rightContent = op.currentBlock.html;
303
+ tableRows.push(`<tr><td class="diff-line-empty"></td><td class="diff-line-add markdown-rendered">${rightContent}</td></tr>`);
304
+ }
305
+ });
306
+
307
+ const hasChanges = addCount > 0 || delCount > 0;
308
+ const counters = hasChanges
309
+ ? `${addCount} block additions, ${delCount} block deletions`
310
+ : 'No differences';
311
+
312
+ return {
313
+ tableHtml: tableRows.join('\n'),
314
+ counters,
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Convert HTML to rendered markdown HTML (for display)
320
+ * @param {string} html - HTML content to convert
321
+ * @param {boolean} [ignoreNavFooter=true] - Whether to filter nav/footer elements
322
+ * @returns {Promise<string>} Rendered markdown HTML
323
+ */
324
+ export async function htmlToRenderedMarkdown(html, ignoreNavFooter = true) {
325
+ // Extract body content only (with nav/footer filtering applied)
326
+ const bodyContent = await filterHtmlContent(html, ignoreNavFooter, false);
327
+
328
+ // Convert to markdown and back to HTML
329
+ const markdown = await htmlToMarkdown(bodyContent);
330
+ return markdownToHtml(markdown);
331
+ }
332
+
333
+ /**
334
+ * Generate complete markdown diff with HTML to Markdown conversion
335
+ * @param {string} originalHtml - Original HTML content
336
+ * @param {string} currentHtml - Current HTML content
337
+ * @param {boolean} [ignoreNavFooter=true] - Whether to filter nav/footer elements
338
+ * @returns {Promise<{originalRenderedHtml: string, currentRenderedHtml: string}>}
339
+ * Rendered markdown HTML for both sides
340
+ */
341
+ export async function generateMarkdownDiff(originalHtml, currentHtml, ignoreNavFooter = true) {
342
+ // Convert both HTMLs to rendered markdown HTML
343
+ const [originalRenderedHtml, currentRenderedHtml] = await Promise.all([
344
+ htmlToRenderedMarkdown(originalHtml, ignoreNavFooter),
345
+ htmlToRenderedMarkdown(currentHtml, ignoreNavFooter),
346
+ ]);
347
+
348
+ return {
349
+ originalRenderedHtml,
350
+ currentRenderedHtml,
351
+ };
352
+ }
package/src/utils.js CHANGED
@@ -60,3 +60,19 @@ export function formatNumberToK(num) {
60
60
  export function isBrowser() {
61
61
  return typeof window !== 'undefined' && typeof document !== 'undefined';
62
62
  }
63
+
64
+ /**
65
+ * Get global object in a cross-platform way
66
+ * @returns {Object} Global object
67
+ */
68
+ export function getGlobalObject() {
69
+ // eslint-disable-next-line no-undef
70
+ if (typeof globalThis !== 'undefined') return globalThis;
71
+ // eslint-disable-next-line no-undef
72
+ if (typeof self !== 'undefined') return self;
73
+ // eslint-disable-next-line no-undef
74
+ if (typeof window !== 'undefined') return window;
75
+ // eslint-disable-next-line no-undef
76
+ if (typeof global !== 'undefined') return global;
77
+ return {};
78
+ }