@microlink/google 0.1.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.
package/src/index.d.ts CHANGED
@@ -90,6 +90,7 @@ interface SearchResult {
90
90
  url: string
91
91
  description: string
92
92
  html(): Promise<string>
93
+ markdown(): Promise<string>
93
94
  }
94
95
 
95
96
  interface SearchPage {
@@ -98,6 +99,7 @@ interface SearchPage {
98
99
  peopleAlsoAsk?: PeopleAlsoAsk[]
99
100
  relatedSearches?: RelatedSearch[]
100
101
  html(): Promise<string>
102
+ markdown(): Promise<string>
101
103
  next(): Promise<SearchPage>
102
104
  }
103
105
 
@@ -109,11 +111,13 @@ interface NewsResult {
109
111
  publisher: string
110
112
  image?: { url: string }
111
113
  html(): Promise<string>
114
+ markdown(): Promise<string>
112
115
  }
113
116
 
114
117
  interface NewsPage {
115
118
  results: NewsResult[]
116
119
  html(): Promise<string>
120
+ markdown(): Promise<string>
117
121
  next(): Promise<NewsPage>
118
122
  }
119
123
 
@@ -126,11 +130,13 @@ interface ImageResult {
126
130
  creator?: string
127
131
  credit?: string
128
132
  html(): Promise<string>
133
+ markdown(): Promise<string>
129
134
  }
130
135
 
131
136
  interface ImagesPage {
132
137
  results: ImageResult[]
133
138
  html(): Promise<string>
139
+ markdown(): Promise<string>
134
140
  next(): Promise<ImagesPage>
135
141
  }
136
142
 
@@ -146,11 +152,13 @@ interface VideoResult {
146
152
  channel?: string
147
153
  date?: string
148
154
  html(): Promise<string>
155
+ markdown(): Promise<string>
149
156
  }
150
157
 
151
158
  interface VideosPage {
152
159
  results: VideoResult[]
153
160
  html(): Promise<string>
161
+ markdown(): Promise<string>
154
162
  next(): Promise<VideosPage>
155
163
  }
156
164
 
@@ -163,11 +171,13 @@ interface PlaceResult {
163
171
  url?: string
164
172
  cid: string
165
173
  html(): Promise<string>
174
+ markdown(): Promise<string>
166
175
  }
167
176
 
168
177
  interface PlacesPage {
169
178
  results: PlaceResult[]
170
179
  html(): Promise<string>
180
+ markdown(): Promise<string>
171
181
  next(): Promise<PlacesPage>
172
182
  }
173
183
 
@@ -190,11 +200,13 @@ interface MapPlaceResult {
190
200
  fid?: string
191
201
  place?: { id: string }
192
202
  html(): Promise<string>
203
+ markdown(): Promise<string>
193
204
  }
194
205
 
195
206
  interface MapsPage {
196
207
  results: MapPlaceResult[]
197
208
  html(): Promise<string>
209
+ markdown(): Promise<string>
198
210
  next(): Promise<MapsPage>
199
211
  }
200
212
 
@@ -207,11 +219,13 @@ interface ShoppingResult {
207
219
  rating?: { score: number; total: number; reviews?: number }
208
220
  id?: string
209
221
  html(): Promise<string>
222
+ markdown(): Promise<string>
210
223
  }
211
224
 
212
225
  interface ShoppingPage {
213
226
  results: ShoppingResult[]
214
227
  html(): Promise<string>
228
+ markdown(): Promise<string>
215
229
  next(): Promise<ShoppingPage>
216
230
  }
217
231
 
@@ -225,11 +239,13 @@ interface ScholarResult {
225
239
  pdf?: { url: string }
226
240
  id: string
227
241
  html(): Promise<string>
242
+ markdown(): Promise<string>
228
243
  }
229
244
 
230
245
  interface ScholarPage {
231
246
  results: ScholarResult[]
232
247
  html(): Promise<string>
248
+ markdown(): Promise<string>
233
249
  next(): Promise<ScholarPage>
234
250
  }
235
251
 
@@ -249,11 +265,13 @@ interface PatentResult {
249
265
  figures?: Array<{ image: { url: string }; thumbnail: { url: string } }>
250
266
  id?: string
251
267
  html(): Promise<string>
268
+ markdown(): Promise<string>
252
269
  }
253
270
 
254
271
  interface PatentsPage {
255
272
  results: PatentResult[]
256
273
  html(): Promise<string>
274
+ markdown(): Promise<string>
257
275
  next(): Promise<PatentsPage>
258
276
  }
259
277
 
@@ -264,6 +282,7 @@ interface SuggestionResult {
264
282
  interface AutocompletePage {
265
283
  results: SuggestionResult[]
266
284
  html(): Promise<string>
285
+ markdown(): Promise<string>
267
286
  next(): Promise<AutocompletePage>
268
287
  }
269
288
 
package/src/index.js CHANGED
@@ -18,6 +18,14 @@ const buildUrl = (query, { limit, location, type, period } = {}) => {
18
18
 
19
19
  const resultUrl = result => result.url
20
20
 
21
+ const fetchDataField = async (url, mqlOpts, field, attr) => {
22
+ const { data } = await mql(url, {
23
+ ...mqlOpts,
24
+ data: { [field]: { attr } }
25
+ })
26
+ return data[field]
27
+ }
28
+
21
29
  const fetchPage = async (url, mqlOpts, offset, query) => {
22
30
  url.searchParams.set('start', String(offset))
23
31
  const { data } = await mql(url.toString(), mqlOpts)
@@ -25,28 +33,27 @@ const fetchPage = async (url, mqlOpts, offset, query) => {
25
33
 
26
34
  return {
27
35
  ...extra,
28
- html: async () => {
29
- const { data } = await mql(
36
+ html: () =>
37
+ fetchDataField(
30
38
  `https://www.google.com/search?q=${encodeURIComponent(query)}`,
31
- {
32
- ...mqlOpts,
33
- data: { content: { attr: 'html' } }
34
- }
35
- )
36
- return data.content
37
- },
39
+ mqlOpts,
40
+ 'content',
41
+ 'html'
42
+ ),
43
+ markdown: () =>
44
+ fetchDataField(
45
+ `https://www.google.com/search?q=${encodeURIComponent(query)}`,
46
+ mqlOpts,
47
+ 'markdown',
48
+ 'markdown'
49
+ ),
38
50
  results: results.map(result => {
39
51
  const url = resultUrl(result)
40
52
  return {
41
53
  ...result,
42
54
  ...(url && {
43
- html: async () => {
44
- const { data } = await mql(url, {
45
- ...mqlOpts,
46
- data: { content: { attr: 'html' } }
47
- })
48
- return data.content
49
- }
55
+ html: () => fetchDataField(url, mqlOpts, 'content', 'html'),
56
+ markdown: () => fetchDataField(url, mqlOpts, 'markdown', 'markdown')
50
57
  })
51
58
  }
52
59
  }),
package/src/js/main.js ADDED
@@ -0,0 +1,397 @@
1
+ const createHeroStats = () => {
2
+ const stats = document.createElement('div')
3
+ stats.className = 'hero-stats reveal-on-load'
4
+ stats.innerHTML = `
5
+ <div class="hero-stat"><strong>10</strong><span>Google verticals</span></div>
6
+ <div class="hero-stat"><strong>~1s</strong><span>Typical latency</span></div>
7
+ <div class="hero-stat hero-stat--serialization"><strong>HTML/MARKDOWN</strong><span>Output serialization</span></div>
8
+ `
9
+ return stats
10
+ }
11
+
12
+ const createVisualBadges = () => {
13
+ const badges = document.createElement('div')
14
+ badges.className = 'hero-badges'
15
+ badges.innerHTML = `
16
+ <div class="hero-badge top">10 products</div>
17
+ <div class="hero-badge bottom">&lt;1s response</div>
18
+ `
19
+ return badges
20
+ }
21
+
22
+ const decorateHero = section => {
23
+ const hero = section.querySelector('div[align="center"]')
24
+ if (!hero || hero.dataset.enhanced === 'true') return
25
+
26
+ hero.dataset.enhanced = 'true'
27
+ hero.className = 'hero-block'
28
+
29
+ const image = hero.querySelector('img')
30
+ const summary = hero.querySelector('p')
31
+
32
+ const copy = document.createElement('div')
33
+ copy.className = 'hero-copy'
34
+
35
+ const eyebrow = document.createElement('div')
36
+ eyebrow.className = 'hero-eyebrow reveal-on-load'
37
+ eyebrow.textContent = 'npm i @microlink/google'
38
+
39
+ const title = document.createElement('h1')
40
+ title.innerHTML = 'Structured <span>Google</span><br>API for LLMs.'
41
+ title.className = 'reveal-on-load'
42
+
43
+ copy.append(eyebrow, title)
44
+
45
+ if (summary) {
46
+ summary.classList.add('reveal-on-load')
47
+ copy.append(summary)
48
+ }
49
+
50
+ const visual = document.createElement('div')
51
+ visual.className = 'hero-visual reveal-on-load'
52
+ visual.innerHTML = `
53
+ <div class="browser-frame">
54
+ <div class="browser-bar">
55
+ <span class="browser-dots"><i></i><i></i><i></i></span>
56
+ <span class="browser-url">https://search.microlink.io</span>
57
+ </div>
58
+ <div class="browser-screen"></div>
59
+ </div>
60
+ `
61
+
62
+ if (image) {
63
+ image.className = 'hero-banner'
64
+ visual.querySelector('.browser-screen').append(image)
65
+ }
66
+
67
+ visual.append(createVisualBadges())
68
+
69
+ hero.replaceChildren(copy, visual)
70
+ hero.after(createHeroStats())
71
+ }
72
+
73
+ const decorateUseCases = section => {
74
+ const useCasesHeading = [...section.querySelectorAll('h2')].find(
75
+ node => node.textContent.trim().toLowerCase() === 'common llm use cases'
76
+ )
77
+
78
+ if (!useCasesHeading) return
79
+
80
+ const intro = useCasesHeading.nextElementSibling
81
+ const list = intro?.nextElementSibling
82
+ if (!list || list.tagName !== 'UL' || list.dataset.enhanced === 'true') return
83
+
84
+ list.dataset.enhanced = 'true'
85
+ list.classList.add('use-case-grid')
86
+
87
+ const variants = [
88
+ 'use-case-card--agentic',
89
+ 'use-case-card--rag',
90
+ 'use-case-card--news',
91
+ 'use-case-card--local'
92
+ ]
93
+
94
+ ;[...list.children].forEach((item, index) => {
95
+ item.classList.add('use-case-card', 'reveal-on-load')
96
+ if (variants[index]) item.classList.add(variants[index])
97
+
98
+ const link = item.querySelector('a')
99
+ if (link) {
100
+ const title = link.textContent.trim()
101
+ const description = item.textContent.replace(title, '').trim()
102
+ const heading = document.createElement('strong')
103
+
104
+ heading.textContent = title
105
+ link.replaceChildren(heading)
106
+
107
+ if (description) {
108
+ const body = document.createElement('p')
109
+ body.textContent = description
110
+ link.append(body)
111
+ }
112
+
113
+ link.classList.add('use-case-link')
114
+ item.replaceChildren(link)
115
+ }
116
+ })
117
+
118
+ const heroStats = section.querySelector('.hero-stats')
119
+ if (heroStats && !section.querySelector('.hero-use-cases')) {
120
+ const heroUseCases = document.createElement('div')
121
+ const heroIntro = document.createElement('p')
122
+
123
+ heroUseCases.className = 'hero-use-cases reveal-on-load'
124
+ heroIntro.className = 'hero-use-cases__intro'
125
+ heroIntro.textContent =
126
+ 'Explore the most common LLM workflows and jump straight to the tutorial that matches your use case.'
127
+
128
+ list.classList.add('hero-use-cases__grid')
129
+
130
+ heroUseCases.append(heroIntro, list)
131
+ heroStats.after(heroUseCases)
132
+ }
133
+ }
134
+
135
+ const decorateCodeBlocks = section => {
136
+ section.querySelectorAll('pre').forEach(pre => {
137
+ if (pre.parentElement?.classList.contains('code-shell')) return
138
+ const shell = document.createElement('div')
139
+ const header = document.createElement('div')
140
+ const code = pre.querySelector('code')
141
+ const languageClass = [...(code?.classList || [])].find(value =>
142
+ value.startsWith('language-')
143
+ )
144
+ shell.className = 'code-shell'
145
+ header.className = 'code-shell__header'
146
+ header.innerHTML = `
147
+ <span class="code-shell__dots" aria-hidden="true"><i></i><i></i><i></i></span>
148
+ <span class="code-shell__label">${
149
+ languageClass ? languageClass.replace('language-', '') : 'code'
150
+ }</span>
151
+ `
152
+ pre.parentNode.insertBefore(shell, pre)
153
+ shell.append(header, pre)
154
+ })
155
+ }
156
+
157
+ const decorateTables = section => {
158
+ section.querySelectorAll('table').forEach(table => {
159
+ if (table.parentElement?.classList.contains('table-shell')) return
160
+ const shell = document.createElement('div')
161
+ shell.className = 'table-shell'
162
+ table.parentNode.insertBefore(shell, table)
163
+ shell.append(table)
164
+ })
165
+ }
166
+
167
+ const revealContent = section => {
168
+ ;[...section.querySelectorAll('h2, h3')].forEach(node =>
169
+ node.classList.add('reveal-on-load')
170
+ )
171
+ }
172
+
173
+ const disableDocsifyDefaultScrollSpy = section => {
174
+ section
175
+ .querySelectorAll(':is(h1, h2, h3, h4, h5) > a.anchor[data-id]')
176
+ .forEach(anchor => {
177
+ anchor.classList.remove('anchor')
178
+ })
179
+ }
180
+
181
+ let teardownNavigationTracking = () => {}
182
+
183
+ const decodeValue = value => {
184
+ try {
185
+ return decodeURIComponent(value)
186
+ } catch {
187
+ return value
188
+ }
189
+ }
190
+
191
+ const extractSectionIdFromHref = href => {
192
+ if (!href) return null
193
+
194
+ const value = href.trim()
195
+ const hashIndex = value.indexOf('#')
196
+ const hash = hashIndex >= 0 ? value.slice(hashIndex + 1) : value
197
+
198
+ if (!hash) return null
199
+
200
+ const isRouteHash = hash.startsWith('/')
201
+ const normalizedHash = isRouteHash ? hash.slice(1) : hash
202
+ const [, query = ''] = normalizedHash.split('?')
203
+ const idFromQuery = query ? new URLSearchParams(query).get('id') : null
204
+
205
+ if (idFromQuery) return decodeValue(idFromQuery)
206
+ if (isRouteHash) return null
207
+
208
+ const [idCandidate = ''] = normalizedHash.split('?')
209
+ return idCandidate ? decodeValue(idCandidate) : null
210
+ }
211
+
212
+ const setupNavigationTracking = section => {
213
+ const sidebarNav = document.querySelector('.sidebar .sidebar-nav')
214
+ if (!sidebarNav) return () => {}
215
+
216
+ const navLinks = [...sidebarNav.querySelectorAll('a[href]')]
217
+ const trackedLinks = navLinks
218
+ .map(link => ({
219
+ id: extractSectionIdFromHref(link.getAttribute('href')),
220
+ link
221
+ }))
222
+ .filter(entry => entry.id)
223
+
224
+ const trackedIds = new Set(trackedLinks.map(entry => entry.id))
225
+ const headings = [...section.querySelectorAll('h2[id], h3[id]')]
226
+ const headingsToTrack = headings.filter(heading => trackedIds.has(heading.id))
227
+ const scopedHeadings = headingsToTrack.length > 0 ? headingsToTrack : headings
228
+ const firstHeadingId = scopedHeadings[0]?.id || trackedLinks[0]?.id || null
229
+
230
+ let activeId = null
231
+
232
+ const setActiveLink = id => {
233
+ navLinks.forEach(link => {
234
+ link.classList.remove('active')
235
+ link.classList.remove('is-active')
236
+ link.removeAttribute('aria-current')
237
+ link.closest('li')?.classList.remove('active')
238
+ link.closest('li')?.classList.remove('is-active')
239
+ })
240
+
241
+ const targetId = id || firstHeadingId
242
+ const activeLink =
243
+ trackedLinks.find(entry => entry.id === targetId)?.link ||
244
+ navLinks.find(link => link.closest('li')?.classList.contains('active')) ||
245
+ navLinks[0]
246
+
247
+ if (!activeLink) return
248
+
249
+ activeLink.classList.add('active', 'is-active')
250
+ activeLink.setAttribute('aria-current', 'location')
251
+ const activeItem = activeLink.closest('li')
252
+
253
+ activeItem?.classList.add('active', 'is-active')
254
+
255
+ let parentItem = activeItem?.parentElement?.closest('li')
256
+ while (parentItem) {
257
+ parentItem.classList.add('active')
258
+ parentItem = parentItem.parentElement?.closest('li')
259
+ }
260
+
261
+ activeId = extractSectionIdFromHref(activeLink.getAttribute('href'))
262
+ }
263
+
264
+ const getCurrentSectionId = () => {
265
+ const triggerOffset = Math.max(8, window.innerHeight * 0.03)
266
+ const threshold = window.scrollY + triggerOffset
267
+ let currentSectionId = null
268
+
269
+ for (const heading of scopedHeadings) {
270
+ if (heading.offsetTop <= threshold) {
271
+ currentSectionId = heading.id
272
+ continue
273
+ }
274
+ break
275
+ }
276
+
277
+ return currentSectionId
278
+ }
279
+
280
+ let frameId = null
281
+
282
+ const syncActiveLink = () => {
283
+ frameId = null
284
+
285
+ if (scopedHeadings.length === 0) {
286
+ setActiveLink(null)
287
+ return
288
+ }
289
+
290
+ const sectionId = getCurrentSectionId()
291
+ const firstHeadingTop = scopedHeadings[0].offsetTop
292
+ const nearTopThreshold = Math.max(8, window.innerHeight * 0.03)
293
+ const isBeforeFirstHeading =
294
+ window.scrollY + nearTopThreshold < firstHeadingTop
295
+
296
+ if (isBeforeFirstHeading) {
297
+ setActiveLink(firstHeadingId)
298
+ return
299
+ }
300
+
301
+ if (sectionId) {
302
+ setActiveLink(sectionId)
303
+ return
304
+ }
305
+
306
+ if (!activeId) {
307
+ setActiveLink(firstHeadingId)
308
+ }
309
+ }
310
+
311
+ const requestSync = () => {
312
+ if (frameId !== null) return
313
+ frameId = window.requestAnimationFrame(syncActiveLink)
314
+ }
315
+
316
+ const handleNavClick = event => {
317
+ const link = event.target.closest('a[href]')
318
+ if (!link || !sidebarNav.contains(link)) return
319
+
320
+ const sectionId = extractSectionIdFromHref(link.getAttribute('href'))
321
+ if (sectionId) setActiveLink(sectionId)
322
+ }
323
+
324
+ sidebarNav.addEventListener('click', handleNavClick)
325
+ window.addEventListener('scroll', requestSync, { passive: true })
326
+ window.addEventListener('resize', requestSync)
327
+
328
+ requestSync()
329
+ window.setTimeout(requestSync, 140)
330
+
331
+ return () => {
332
+ sidebarNav.removeEventListener('click', handleNavClick)
333
+ window.removeEventListener('scroll', requestSync)
334
+ window.removeEventListener('resize', requestSync)
335
+ if (frameId !== null) window.cancelAnimationFrame(frameId)
336
+ }
337
+ }
338
+
339
+ const enhancePage = () => {
340
+ teardownNavigationTracking()
341
+ teardownNavigationTracking = () => {}
342
+
343
+ const section = document.querySelector('.markdown-section')
344
+ const content = document.querySelector('.content')
345
+ const sidebar = document.querySelector('.sidebar')
346
+ if (!section) return
347
+
348
+ if (content) {
349
+ content.id = 'main-content'
350
+ content.setAttribute('role', 'main')
351
+ content.setAttribute('tabindex', '-1')
352
+ }
353
+
354
+ if (sidebar) {
355
+ sidebar.setAttribute('role', 'navigation')
356
+ sidebar.setAttribute('aria-label', 'Primary navigation')
357
+ }
358
+
359
+ decorateHero(section)
360
+ decorateUseCases(section)
361
+ decorateCodeBlocks(section)
362
+ decorateTables(section)
363
+ revealContent(section)
364
+ disableDocsifyDefaultScrollSpy(section)
365
+ teardownNavigationTracking = setupNavigationTracking(section)
366
+ }
367
+
368
+ window.$docsify = {
369
+ name: '@microlink/google',
370
+ repo: 'microlinkhq/google',
371
+ logo: '/static/banner.png',
372
+ externalLinkRel: 'noopener noreferrer',
373
+ subMaxLevel: 2,
374
+ auto2top: true,
375
+ maxLevel: 3,
376
+ plugins: [
377
+ hook => {
378
+ hook.doneEach(() => {
379
+ enhancePage()
380
+ const appLink = document.querySelector('.app-name-link')
381
+ if (appLink && !appLink.dataset.bound) {
382
+ appLink.dataset.bound = 'true'
383
+ appLink.onclick = event => {
384
+ event.preventDefault()
385
+ const shouldReduceMotion = window.matchMedia(
386
+ '(prefers-reduced-motion: reduce)'
387
+ ).matches
388
+ window.scrollTo({
389
+ top: 0,
390
+ behavior: shouldReduceMotion ? 'auto' : 'smooth'
391
+ })
392
+ }
393
+ }
394
+ })
395
+ }
396
+ ]
397
+ }