@aglyn/plugins-ai 1.0.0-beta.288 → 1.0.0-beta.290

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 (92) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +3 -2
  3. package/src/lib/activity/ai-activity-actions.js +3 -0
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/components/ai-job-requests.d.ts +2 -0
  6. package/src/lib/components/ai-job-requests.js +2 -0
  7. package/src/lib/components/ai-job-requests.js.map +1 -1
  8. package/src/lib/components/ai-site-build-page.component.js +4 -0
  9. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  10. package/src/lib/components/ai-site-import-job.component.d.ts +6 -1
  11. package/src/lib/components/ai-site-import-job.component.js +124 -17
  12. package/src/lib/components/ai-site-import-job.component.js.map +1 -1
  13. package/src/lib/components/ai-site-import-start.component.d.ts +6 -0
  14. package/src/lib/components/ai-site-import-start.component.js +18 -4
  15. package/src/lib/components/ai-site-import-start.component.js.map +1 -1
  16. package/src/lib/components/ai-site-start-card.component.js +2 -1
  17. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  18. package/src/lib/jobs/ai-image-reads.d.ts +20 -2
  19. package/src/lib/jobs/ai-image-reads.js +47 -0
  20. package/src/lib/jobs/ai-image-reads.js.map +1 -1
  21. package/src/lib/jobs/ai-job-import-step.d.ts +53 -3
  22. package/src/lib/jobs/ai-job-import-step.js +386 -64
  23. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-layout-language.d.ts +33 -0
  25. package/src/lib/jobs/ai-job-layout-language.js +75 -1
  26. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  27. package/src/lib/jobs/ai-job-layout-step.js +8 -3
  28. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  29. package/src/lib/jobs/ai-job-page-language.d.ts +29 -0
  30. package/src/lib/jobs/ai-job-page-language.js +178 -2
  31. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  32. package/src/lib/jobs/ai-job-page-step.js +3 -0
  33. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  34. package/src/lib/jobs/ai-site-import-document.d.ts +117 -0
  35. package/src/lib/jobs/ai-site-import-document.js +710 -0
  36. package/src/lib/jobs/ai-site-import-document.js.map +1 -0
  37. package/src/lib/jobs/ai-site-import-look.d.ts +112 -0
  38. package/src/lib/jobs/ai-site-import-look.js +402 -0
  39. package/src/lib/jobs/ai-site-import-look.js.map +1 -0
  40. package/src/lib/jobs/ai-site-import-publish.d.ts +41 -0
  41. package/src/lib/jobs/ai-site-import-publish.js +92 -0
  42. package/src/lib/jobs/ai-site-import-publish.js.map +1 -0
  43. package/src/lib/jobs/ai-theme-brand-inputs.d.ts +27 -2
  44. package/src/lib/jobs/ai-theme-brand-inputs.js +37 -8
  45. package/src/lib/jobs/ai-theme-brand-inputs.js.map +1 -1
  46. package/src/lib/layout-language/ai-layout-frame.d.ts +17 -0
  47. package/src/lib/layout-language/ai-layout-frame.js +73 -2
  48. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  49. package/src/lib/layout-language/ai-layout-language.d.ts +43 -0
  50. package/src/lib/layout-language/ai-layout-language.js +199 -0
  51. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  52. package/src/lib/model/ai-credit-estimate.d.ts +15 -0
  53. package/src/lib/model/ai-credit-estimate.js +14 -1
  54. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  55. package/src/lib/model/ai-jobs.types.d.ts +13 -1
  56. package/src/lib/model/ai-jobs.types.js.map +1 -1
  57. package/src/lib/model/ai-site-import-view.d.ts +47 -3
  58. package/src/lib/model/ai-site-import-view.js +122 -20
  59. package/src/lib/model/ai-site-import-view.js.map +1 -1
  60. package/src/lib/model/ai-site-import.d.ts +232 -7
  61. package/src/lib/model/ai-site-import.js +365 -35
  62. package/src/lib/model/ai-site-import.js.map +1 -1
  63. package/src/lib/runtime/ai-doctrine-validators.d.ts +18 -0
  64. package/src/lib/runtime/ai-doctrine-validators.js +16 -5
  65. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  66. package/src/lib/runtime/ai-palette.generated.js +31 -0
  67. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  68. package/src/lib/server/ai-image-reads-route.js +24 -1
  69. package/src/lib/server/ai-image-reads-route.js.map +1 -1
  70. package/src/lib/server/ai-jobs-resume.js +7 -1
  71. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  72. package/src/lib/site-import/fixtures/aglyn-com-recordings.d.ts +57 -0
  73. package/src/lib/site-import/fixtures/aglyn-com-recordings.js +172 -0
  74. package/src/lib/site-import/fixtures/aglyn-com-recordings.js.map +1 -0
  75. package/src/lib/site-import/site-import-crawl.d.ts +32 -7
  76. package/src/lib/site-import/site-import-crawl.js +133 -44
  77. package/src/lib/site-import/site-import-crawl.js.map +1 -1
  78. package/src/lib/site-import/site-import-edge-server.d.ts +31 -0
  79. package/src/lib/site-import/site-import-edge-server.js +31 -0
  80. package/src/lib/site-import/site-import-edge-server.js.map +1 -0
  81. package/src/lib/site-import/site-import-edge.d.ts +44 -0
  82. package/src/lib/site-import/site-import-edge.js +108 -0
  83. package/src/lib/site-import/site-import-edge.js.map +1 -0
  84. package/src/lib/site-import/site-import-html.d.ts +73 -3
  85. package/src/lib/site-import/site-import-html.js +332 -45
  86. package/src/lib/site-import/site-import-html.js.map +1 -1
  87. package/src/lib/site-import/site-import-images.d.ts +22 -0
  88. package/src/lib/site-import/site-import-images.js +78 -2
  89. package/src/lib/site-import/site-import-images.js.map +1 -1
  90. package/src/lib/site-import/site-import-page.d.ts +44 -2
  91. package/src/lib/site-import/site-import-page.js +116 -16
  92. package/src/lib/site-import/site-import-page.js.map +1 -1
@@ -32,15 +32,25 @@
32
32
  *
33
33
  * The output is DATA. Text from a page reaches a model only fenced as
34
34
  * untrusted (`fenceUntrustedText`), never as instructions.
35
+ *
36
+ * What a page is made of is kept as the page shows it (AGL-4083): words that
37
+ * sit in tags side by side stay apart ("Sign in" and "Start free", never
38
+ * "Sign inStart free"); a link is a block of its own, `mailto:` and `tel:`
39
+ * ones too, so an address or a phone number survives; a menu entry is named
40
+ * by its first line, not by its mega-menu description; a `<picture>`'s
41
+ * source stands in for an `<img>` with no address; and the head's share card,
42
+ * icons, site name and Organization structured data come along for the
43
+ * header, the footer and the site's own facts.
35
44
  *=========================================*/ /** One run of a page's content, in the order the page draws it. */ import { _ as _extends } from "@swc/helpers/_/_extends";
36
45
  /** Ceilings that keep one odd page from becoming the whole inventory. */ export const SITE_IMPORT_PAGE_LIMITS = {
37
- blocks: 400,
38
- blockChars: 2000,
46
+ /** A pricing page's plan table runs past 400 (AGL-4083); a wave's read keeps fewer, under its own budget. */ blocks: 2000,
47
+ /** A legal page's paragraph runs past 2,000 characters. */ blockChars: 10000,
39
48
  images: 100,
40
49
  links: 300,
41
- navLinks: 60,
50
+ /** A mega-menu's entries and the footer's columns together. */ navLinks: 150,
42
51
  forms: 10,
43
- fieldsPerForm: 30
52
+ fieldsPerForm: 30,
53
+ icons: 12
44
54
  };
45
55
  /** Elements removed with everything inside them. */ const REMOVED_WITH_CONTENT = [
46
56
  'script',
@@ -105,6 +115,52 @@ const BLOCK_TAGS = {
105
115
  'br',
106
116
  'body'
107
117
  ]);
118
+ /**
119
+ * Tags a browser lays out apart from the words beside them (AGL-4083): two
120
+ * links, or a link and a picture, side by side are two runs of words, never
121
+ * one ("Sign in" and "Start free", not "Sign inStart free"). Every other tag
122
+ * inside a block styles words where they are (a `<span>` or a `<strong>`
123
+ * mid-sentence) and adds no space; block-level tags already end their block.
124
+ */ const RUN_BOUNDARY = new Set([
125
+ 'a',
126
+ 'img',
127
+ 'button',
128
+ 'input',
129
+ 'select',
130
+ 'textarea',
131
+ 'picture',
132
+ 'video',
133
+ 'audio',
134
+ 'iframe',
135
+ 'object',
136
+ 'embed',
137
+ 'canvas',
138
+ 'svg',
139
+ 'meter',
140
+ 'progress',
141
+ 'output'
142
+ ]);
143
+ /** Where one run of words ends and the next begins, resolved when a block is written. */ const RUN_BREAK = '\u0001';
144
+ /**
145
+ * The runs of a block joined as a reader sees them: a space between two
146
+ * runs, except before closing punctuation or after an opening one, where
147
+ * the page wrote none ("<span>Austin</span>, Texas" is "Austin, Texas").
148
+ */ const RUN_BREAKS = new RegExp(`[\\s${RUN_BREAK}]*${RUN_BREAK}[\\s${RUN_BREAK}]*`, 'g');
149
+ function joinRuns(text) {
150
+ if (!text.includes(RUN_BREAK)) return text;
151
+ return text.replace(RUN_BREAKS, (match, offset, whole)=>{
152
+ var _whole_, _whole_1, _whole_2, _whole_3;
153
+ if (/\s/.test(match)) return ' ';
154
+ const before = (_whole_ = whole[offset - 1]) != null ? _whole_ : '';
155
+ const after = (_whole_1 = whole[offset + match.length]) != null ? _whole_1 : '';
156
+ if (!before || !after) return '';
157
+ if (/[.,;:!?%)\]}\u2019\u201d\u00bb]/.test(after) || /[([{\u2018\u201c\u00ab]/.test(before)) return '';
158
+ // A straight quote opens after a space or a bracket, and closes before one.
159
+ if (/["']/.test(before) && /^$|[\s([{]/.test((_whole_2 = whole[offset - 2]) != null ? _whole_2 : '')) return '';
160
+ if (/["']/.test(after) && /^$|[\s.,;:!?)\]}]/.test((_whole_3 = whole[offset + match.length + 1]) != null ? _whole_3 : '')) return '';
161
+ return ' ';
162
+ });
163
+ }
108
164
  const NAMED_ENTITIES = {
109
165
  amp: '&',
110
166
  lt: '<',
@@ -176,7 +232,7 @@ const TAG = /<(\/?)([a-zA-Z][\w:-]*)((?:[^>"']|"[^"]*"|'[^']*')*)>/g;
176
232
  }).filter(Boolean);
177
233
  }
178
234
  function collapse(text) {
179
- return text.replace(/\s+/g, ' ').trim();
235
+ return joinRuns(text).replace(/\s+/g, ' ').trim();
180
236
  }
181
237
  function clip(text, max) {
182
238
  return text.length > max ? `${text.slice(0, max - 1).trimEnd()}…` : text;
@@ -203,9 +259,12 @@ function removeElements(html, tags) {
203
259
  if (kept.length < SITE_IMPORT_PAGE_LIMITS.forms) {
204
260
  const fields = [];
205
261
  for (const match of form.matchAll(/<(input|textarea|select)\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi)){
262
+ var _attributes_ariahidden;
206
263
  const attributes = parseAttributes(match[2]);
207
264
  const type = match[1].toLowerCase() === 'input' ? (attributes['type'] || 'text').toLowerCase() : match[1].toLowerCase();
208
265
  if (type === 'hidden' || type === 'submit' || type === 'button' || type === 'image') continue;
266
+ // A field no visitor sees is a trap for bots or a widget's shadow, not a question (AGL-4083).
267
+ if (((_attributes_ariahidden = attributes['aria-hidden']) != null ? _attributes_ariahidden : '').toLowerCase() === 'true' || attributes['tabindex'] === '-1') continue;
209
268
  fields.push({
210
269
  type,
211
270
  name: clip(attributes['name'] || attributes['id'] || '', 80)
@@ -241,19 +300,154 @@ function removeElements(html, tags) {
241
300
  const first = (_String_split_ = String(srcset != null ? srcset : '').split(',')[0]) == null ? void 0 : _String_split_.trim().split(/\s+/)[0];
242
301
  return first || undefined;
243
302
  }
303
+ /**
304
+ * Where a link a page shows goes (AGL-4083): an absolute http(s) address
305
+ * without its fragment, a `mailto:` with an address in it, or a `tel:` with
306
+ * a number in it; `null` for anything else (a script, a jump on the page).
307
+ */ export function resolveSiteLink(raw, base) {
308
+ var _ref;
309
+ var _resolveSiteUrl;
310
+ const value = String(raw != null ? raw : '').trim();
311
+ const mail = /^mailto:([^?\s<>"']+@[^?\s<>"']+)/i.exec(value);
312
+ if (mail) return `mailto:${decodeURIComponent(mail[1]).toLowerCase()}`;
313
+ const phone = /^tel:([+\d][\d\s().-]{2,30})$/i.exec(value);
314
+ if (phone) return `tel:${phone[1].replace(/[\s().-]/g, '')}`;
315
+ if (value.startsWith('#')) return null;
316
+ return (_ref = (_resolveSiteUrl = resolveSiteUrl(value, base)) == null ? void 0 : _resolveSiteUrl.toString()) != null ? _ref : null;
317
+ }
318
+ /**
319
+ * A `<picture>` whose `<img>` has no address of its own (a placeholder, or
320
+ * none at all) is read from its first `<source>` (AGL-4083); a `<picture>`
321
+ * whose `<img>` has one keeps it. The sources go with the void elements.
322
+ */ function readPictures(html) {
323
+ return html.replace(/<picture\b(?:[^>"']|"[^"]*"|'[^']*')*>([\s\S]*?)<\/picture\s*>/gi, (whole, inner)=>{
324
+ var _attributes_alt;
325
+ const img = /<img\b((?:[^>"']|"[^"]*"|'[^']*')*)>/i.exec(inner);
326
+ const attributes = img ? parseAttributes(img[1]) : {};
327
+ const own = attributes['src'] || attributes['data-src'] || firstSrcset(attributes['srcset'] || attributes['data-srcset']);
328
+ if (own && !/^data:/i.test(own.trim())) return whole;
329
+ let source;
330
+ for (const match of inner.matchAll(/<source\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi)){
331
+ const sourceAttributes = parseAttributes(match[1]);
332
+ source = firstSrcset(sourceAttributes['srcset'] || sourceAttributes['data-srcset']);
333
+ if (source) break;
334
+ }
335
+ if (!source) return whole;
336
+ const quote = (value)=>value.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
337
+ return ` <img src="${quote(source)}" alt="${quote((_attributes_alt = attributes['alt']) != null ? _attributes_alt : '')}"> `;
338
+ });
339
+ }
340
+ /** The head's icons, the largest first within each kind. */ function readIcons(html, pageUrl) {
341
+ const icons = [];
342
+ for (const match of html.matchAll(/<link\b((?:[^>"']|"[^"]*"|'[^']*')*)>/gi)){
343
+ var _attributes_rel, _match, _attributes_sizes;
344
+ var _attributes_type;
345
+ const attributes = parseAttributes(match[1]);
346
+ const rels = ((_attributes_rel = attributes['rel']) != null ? _attributes_rel : '').toLowerCase().split(/\s+/);
347
+ const rel = rels.includes('apple-touch-icon') || rels.includes('apple-touch-icon-precomposed') ? 'apple-touch-icon' : rels.includes('icon') ? 'icon' : null;
348
+ if (!rel) continue;
349
+ const url = resolveSiteUrl(attributes['href'], pageUrl);
350
+ if (!url) continue;
351
+ const sizes = [
352
+ ...(_match = ((_attributes_sizes = attributes['sizes']) != null ? _attributes_sizes : '').match(/(\d+)\s*x\s*(\d+)/gi)) != null ? _match : []
353
+ ];
354
+ const size = sizes.reduce((most, entry)=>Math.max(most, ...entry.split(/x/i).map((side)=>Number(side) || 0)), 0);
355
+ icons.push({
356
+ url: url.toString(),
357
+ rel,
358
+ size: size || null,
359
+ type: ((_attributes_type = attributes['type']) == null ? void 0 : _attributes_type.trim()) || null
360
+ });
361
+ if (icons.length >= SITE_IMPORT_PAGE_LIMITS.icons) break;
362
+ }
363
+ return icons.sort((a, b)=>{
364
+ var _b_size, _a_size;
365
+ return a.rel === b.rel ? ((_b_size = b.size) != null ? _b_size : 0) - ((_a_size = a.size) != null ? _a_size : 0) : a.rel === 'apple-touch-icon' ? -1 : 1;
366
+ });
367
+ }
368
+ const ORGANIZATION_TYPE = /(?:^|\b)(?:Organization|Corporation|LocalBusiness|NGO|EducationalOrganization|MedicalOrganization|SportsOrganization|Store|Restaurant|ProfessionalService|HomeAndConstructionBusiness|HealthAndBeautyBusiness|FoodEstablishment|AutomotiveBusiness|LegalService|FinancialService|Dentist|Physician|LodgingBusiness|EntertainmentBusiness)$/;
369
+ /**
370
+ * The first Organization (or kind of local business) the page's structured
371
+ * data describes, read before scripts are thrown away. Malformed data, or
372
+ * none, is `null`; nothing in it is ever run.
373
+ */ function readOrganization(html) {
374
+ var _node_contactPoint, _node_sameAs;
375
+ const nodes = [];
376
+ const visit = (value, depth)=>{
377
+ if (depth > 4 || nodes.length > 50 || !value || typeof value !== 'object') return;
378
+ if (Array.isArray(value)) {
379
+ for (const entry of value)visit(entry, depth + 1);
380
+ return;
381
+ }
382
+ const node = value;
383
+ nodes.push(node);
384
+ if (node['@graph']) visit(node['@graph'], depth + 1);
385
+ };
386
+ for (const match of html.matchAll(/<script\b[^>]*\btype\s*=\s*["']?application\/ld\+json["']?[^>]*>([\s\S]*?)<\/script\s*>/gi)){
387
+ try {
388
+ visit(JSON.parse(match[1]), 0);
389
+ } catch (unused) {
390
+ // Structured data that does not parse says nothing.
391
+ }
392
+ }
393
+ const typeOf = (node)=>{
394
+ var _node_type;
395
+ return [].concat((_node_type = node['@type']) != null ? _node_type : []).map(String);
396
+ };
397
+ const node = nodes.find((entry)=>typeOf(entry).some((type)=>ORGANIZATION_TYPE.test(type)));
398
+ if (!node) return null;
399
+ const text = (value, max = 300)=>typeof value === 'string' ? clip(collapse(value), max) : '';
400
+ const urlOf = (value)=>{
401
+ const raw = typeof value === 'string' ? value : value && typeof value === 'object' ? value['url'] : '';
402
+ return typeof raw === 'string' && /^https?:\/\//i.test(raw.trim()) ? raw.trim() : '';
403
+ };
404
+ const contact = [].concat((_node_contactPoint = node['contactPoint']) != null ? _node_contactPoint : []).find((entry)=>entry && typeof entry === 'object');
405
+ const address = (()=>{
406
+ const value = node['address'];
407
+ if (typeof value === 'string') return text(value);
408
+ if (!value || typeof value !== 'object') return '';
409
+ const parts = value;
410
+ const region = [
411
+ text(parts['addressRegion']),
412
+ text(parts['postalCode'])
413
+ ].filter(Boolean).join(' ');
414
+ const country = parts['addressCountry'];
415
+ return [
416
+ text(parts['streetAddress']),
417
+ text(parts['addressLocality']),
418
+ region,
419
+ typeof country === 'string' ? text(country) : text(country == null ? void 0 : country['name'])
420
+ ].filter(Boolean).join(', ');
421
+ })();
422
+ return {
423
+ name: text(node['name']),
424
+ description: text(node['description'], 1000),
425
+ url: urlOf(node['url']),
426
+ logo: urlOf(node['logo']),
427
+ telephone: text(node['telephone']) || text(contact == null ? void 0 : contact['telephone']),
428
+ email: (text(node['email']) || text(contact == null ? void 0 : contact['email'])).replace(/^mailto:/i, ''),
429
+ address,
430
+ sameAs: [].concat((_node_sameAs = node['sameAs']) != null ? _node_sameAs : []).map((entry)=>urlOf(entry)).filter(Boolean).slice(0, 20)
431
+ };
432
+ }
244
433
  /**
245
434
  * The words, structure, pictures and links of one page.
246
435
  *
247
436
  * `isSameSite(url)` says whether an address is on the site being imported;
248
437
  * only those become `links`, and images are marked by it.
249
438
  */ export function extractSiteImportPage(html, pageUrl, isSameSite) {
250
- var _ref;
251
- var _parseAttributes_lang;
439
+ var _ref, _ref1;
440
+ var _resolveSiteUrl, _parseAttributes_lang;
252
441
  const empty = {
253
442
  title: '',
254
443
  description: '',
255
444
  lang: null,
256
445
  canonical: null,
446
+ ogImage: null,
447
+ siteName: '',
448
+ icons: [],
449
+ brand: null,
450
+ organization: null,
257
451
  blocks: [],
258
452
  images: [],
259
453
  navLinks: [],
@@ -268,7 +462,10 @@ function removeElements(html, tags) {
268
462
  if (PASSWORD_INPUT.test(source)) return _extends({}, empty, {
269
463
  loginForm: true
270
464
  });
271
- source = removeElements(source, REMOVED_WITH_CONTENT).replace(REMOVED_VOID, ' ');
465
+ // Read before scripts go: the structured data is a script's body.
466
+ const organization = readOrganization(source);
467
+ const icons = readIcons(source, pageUrl);
468
+ source = removeElements(readPictures(source), REMOVED_WITH_CONTENT).replace(REMOVED_VOID, ' ');
272
469
  // Forms next, so a field list is read before the controls go.
273
470
  const forms = takeForms(source);
274
471
  source = removeElements(forms.html, FORM_CONTROLS);
@@ -284,6 +481,8 @@ function removeElements(html, tags) {
284
481
  const titleMatch = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(source);
285
482
  const title = clip(collapse(decodeHtmlEntities((_ref = titleMatch == null ? void 0 : titleMatch[1]) != null ? _ref : '')) || metaContent('og:title', 'property'), 300);
286
483
  const description = clip(metaContent('description', 'name') || metaContent('og:description', 'property'), 1000);
484
+ const ogImage = (_ref1 = (_resolveSiteUrl = resolveSiteUrl(metaContent('og:image', 'property') || metaContent('og:image:url', 'property') || metaContent('twitter:image', 'name'), pageUrl)) == null ? void 0 : _resolveSiteUrl.toString()) != null ? _ref1 : null;
485
+ const siteName = clip(metaContent('og:site_name', 'property') || metaContent('application-name', 'name'), 120);
287
486
  const htmlTag = /<html\b((?:[^>"']|"[^"]*"|'[^']*')*)>/i.exec(source);
288
487
  const lang = htmlTag ? ((_parseAttributes_lang = parseAttributes(htmlTag[1])['lang']) == null ? void 0 : _parseAttributes_lang.trim()) || null : null;
289
488
  let canonical = null;
@@ -291,9 +490,9 @@ function removeElements(html, tags) {
291
490
  var _attributes_rel;
292
491
  const attributes = parseAttributes(match[1]);
293
492
  if (((_attributes_rel = attributes['rel']) != null ? _attributes_rel : '').toLowerCase().split(/\s+/).includes('canonical')) {
294
- var _ref1;
295
- var _resolveSiteUrl;
296
- canonical = (_ref1 = (_resolveSiteUrl = resolveSiteUrl(attributes['href'], pageUrl)) == null ? void 0 : _resolveSiteUrl.toString()) != null ? _ref1 : null;
493
+ var _ref2;
494
+ var _resolveSiteUrl1;
495
+ canonical = (_ref2 = (_resolveSiteUrl1 = resolveSiteUrl(attributes['href'], pageUrl)) == null ? void 0 : _resolveSiteUrl1.toString()) != null ? _ref2 : null;
297
496
  break;
298
497
  }
299
498
  }
@@ -306,47 +505,90 @@ function removeElements(html, tags) {
306
505
  const navLinks = [];
307
506
  const links = [];
308
507
  const linkSeen = new Set();
508
+ let brand = null;
309
509
  let navDepth = 0;
310
510
  let headerDepth = 0;
311
511
  let footerDepth = 0;
312
512
  let current = null;
313
513
  let buffer = '';
314
- let anchor = null;
514
+ /** The link being read: its menu address (http(s) only), where it goes, its words and its first picture. */ let anchor = null;
515
+ /** The links read into the block being built, written after it. */ let pending = [];
315
516
  const region = ()=>footerDepth > 0 ? 'footer' : headerDepth > 0 ? 'header' : 'main';
316
517
  // A site without a <nav> keeps its menu in the header; its links are the nav.
317
518
  const inNav = ()=>navDepth > 0 || !hasNav && headerDepth > 0;
519
+ const room = ()=>blocks.length < SITE_IMPORT_PAGE_LIMITS.blocks;
318
520
  const flush = ()=>{
319
521
  var _ref;
320
- const text = clip(collapse(decodeHtmlEntities(buffer)), SITE_IMPORT_PAGE_LIMITS.blockChars);
522
+ const raw = buffer;
523
+ const written = pending;
321
524
  buffer = '';
525
+ pending = [];
526
+ if (anchor && collapse(decodeHtmlEntities(raw.slice(anchor.start)))) anchor.crossed = true;
527
+ if (anchor) anchor.start = 0;
322
528
  const kind = (_ref = current == null ? void 0 : current.kind) != null ? _ref : 'paragraph';
323
529
  const level = current == null ? void 0 : current.level;
530
+ const loose = current === null;
324
531
  current = null;
325
- if (!text || navDepth > 0 || blocks.length >= SITE_IMPORT_PAGE_LIMITS.blocks) return;
326
- const last = blocks[blocks.length - 1];
327
- if (last && last.kind !== 'image' && last.text === text) return;
328
- if (kind === 'heading') {
329
- blocks.push({
330
- kind,
331
- level: level != null ? level : 2,
332
- text,
333
- region: region()
334
- });
335
- } else {
336
- blocks.push({
337
- kind,
338
- text,
339
- region: region()
340
- });
532
+ if (navDepth > 0) return;
533
+ const text = clip(collapse(decodeHtmlEntities(raw)), SITE_IMPORT_PAGE_LIMITS.blockChars);
534
+ // Loose words that are only links (a row of buttons, an address in a
535
+ // card) are those links, each a block of its own; words around a link
536
+ // keep their block, and the link follows it.
537
+ let rest = raw;
538
+ for (const link of [
539
+ ...written
540
+ ].sort((a, b)=>b.start - a.start))rest = `${rest.slice(0, link.start)}${rest.slice(link.end)}`;
541
+ const onlyLinks = loose && written.length > 0 && !collapse(decodeHtmlEntities(rest));
542
+ if (text && !onlyLinks && room()) {
543
+ const last = blocks[blocks.length - 1];
544
+ if (!(last && last.kind !== 'image' && last.kind !== 'link' && last.text === text)) {
545
+ if (kind === 'heading') {
546
+ blocks.push({
547
+ kind,
548
+ level: level != null ? level : 2,
549
+ text,
550
+ region: region()
551
+ });
552
+ } else {
553
+ blocks.push({
554
+ kind,
555
+ text,
556
+ region: region()
557
+ });
558
+ }
559
+ }
560
+ }
561
+ for (const link of written){
562
+ if (!room()) break;
563
+ const last = blocks[blocks.length - 1];
564
+ if ((last == null ? void 0 : last.kind) === 'link' && last.href === link.href && last.text === link.text) continue;
565
+ const inline = !onlyLinks || link.crossed;
566
+ blocks.push(_extends({
567
+ kind: 'link',
568
+ text: link.text,
569
+ href: link.href,
570
+ region: link.region
571
+ }, inline ? {
572
+ inline: true
573
+ } : {}));
341
574
  }
342
575
  };
576
+ /** A boundary between two runs of words: in the block, and in the link being read. */ const boundary = (block)=>{
577
+ buffer += RUN_BREAK;
578
+ if (!anchor) return;
579
+ anchor.text += RUN_BREAK;
580
+ if (block && anchor.lines[anchor.lines.length - 1] !== '') anchor.lines.push('');
581
+ };
343
582
  for (const match of body.matchAll(new RegExp(`${TAG.source}|([^<]+)|<`, 'g'))){
344
583
  var _match_;
345
584
  const text = match[4];
346
585
  if (text !== undefined || match[2] === undefined) {
347
586
  const run = text != null ? text : '<';
348
587
  buffer += run;
349
- if (anchor) anchor.text += run;
588
+ if (anchor) {
589
+ anchor.text += run;
590
+ anchor.lines[anchor.lines.length - 1] += run;
591
+ }
350
592
  continue;
351
593
  }
352
594
  const closing = match[1] === '/';
@@ -355,22 +597,51 @@ function removeElements(html, tags) {
355
597
  if (tag === 'a') {
356
598
  if (closing) {
357
599
  if (anchor) {
358
- const label = clip(collapse(decodeHtmlEntities(anchor.text)), 120);
359
- if (inNav() && label && navLinks.length < SITE_IMPORT_PAGE_LIMITS.navLinks) {
600
+ var _anchor_lines_find;
601
+ const label = clip(collapse(decodeHtmlEntities(anchor.text)), 300);
602
+ const firstLine = clip(collapse(decodeHtmlEntities((_anchor_lines_find = anchor.lines.find((line)=>collapse(decodeHtmlEntities(line)))) != null ? _anchor_lines_find : '')), 120);
603
+ if (inNav() && anchor.url && firstLine && navLinks.length < SITE_IMPORT_PAGE_LIMITS.navLinks) {
360
604
  navLinks.push({
361
- text: label,
605
+ text: firstLine,
362
606
  url: anchor.url.toString()
363
607
  });
364
608
  }
609
+ if (!brand && anchor.region !== 'footer' && (headerDepth > 0 || navDepth > 0) && anchor.url && isSameSite(anchor.url) && anchor.url.pathname === '/') {
610
+ brand = {
611
+ logo: anchor.image,
612
+ text: label,
613
+ region: anchor.region
614
+ };
615
+ }
616
+ if (!inNav() && anchor.href && label) {
617
+ pending.push({
618
+ text: label,
619
+ href: anchor.href,
620
+ start: anchor.start,
621
+ end: buffer.length,
622
+ crossed: anchor.crossed,
623
+ region: anchor.region
624
+ });
625
+ }
365
626
  anchor = null;
366
627
  }
628
+ boundary(false);
367
629
  continue;
368
630
  }
631
+ boundary(false);
369
632
  const url = resolveSiteUrl(attributes['href'], pageUrl);
370
- anchor = url ? {
633
+ anchor = {
371
634
  url,
372
- text: ''
373
- } : null;
635
+ href: resolveSiteLink(attributes['href'], pageUrl),
636
+ text: '',
637
+ lines: [
638
+ ''
639
+ ],
640
+ image: null,
641
+ start: buffer.length,
642
+ crossed: false,
643
+ region: region()
644
+ };
374
645
  if (url && isSameSite(url) && links.length < SITE_IMPORT_PAGE_LIMITS.links) {
375
646
  const key = url.toString();
376
647
  if (!linkSeen.has(key)) {
@@ -382,27 +653,31 @@ function removeElements(html, tags) {
382
653
  }
383
654
  if (tag === 'img' && !closing) {
384
655
  const url = resolveSiteUrl(attributes['src'] || attributes['data-src'] || firstSrcset(attributes['srcset'] || attributes['data-srcset']), pageUrl);
656
+ boundary(false);
385
657
  if (url && images.length < SITE_IMPORT_PAGE_LIMITS.images) {
386
658
  var _attributes_alt;
387
659
  const key = url.toString();
388
660
  const alt = clip(collapse((_attributes_alt = attributes['alt']) != null ? _attributes_alt : ''), 300);
661
+ if (anchor && !anchor.image) anchor.image = {
662
+ url: key,
663
+ alt
664
+ };
389
665
  if (!imageSeen.has(key)) {
390
666
  imageSeen.add(key);
391
667
  images.push({
392
668
  url: key,
393
669
  alt,
394
- sameSite: isSameSite(url)
670
+ sameSite: isSameSite(url),
671
+ region: region()
395
672
  });
396
673
  if (navDepth === 0) {
397
674
  flush();
398
- if (blocks.length < SITE_IMPORT_PAGE_LIMITS.blocks) {
399
- blocks.push({
400
- kind: 'image',
401
- url: key,
402
- alt,
403
- region: region()
404
- });
405
- }
675
+ if (room()) blocks.push({
676
+ kind: 'image',
677
+ url: key,
678
+ alt,
679
+ region: region()
680
+ });
406
681
  }
407
682
  }
408
683
  }
@@ -410,6 +685,7 @@ function removeElements(html, tags) {
410
685
  }
411
686
  if (tag === 'nav' || tag === 'header' || tag === 'footer') {
412
687
  flush();
688
+ boundary(true);
413
689
  const step = closing ? -1 : 1;
414
690
  if (tag === 'nav') navDepth = Math.max(0, navDepth + step);
415
691
  if (tag === 'header') headerDepth = Math.max(0, headerDepth + step);
@@ -420,9 +696,11 @@ function removeElements(html, tags) {
420
696
  if (heading || BLOCK_TAGS[tag]) {
421
697
  if (closing) {
422
698
  if ((current == null ? void 0 : current.tag) === tag) flush();
699
+ boundary(true);
423
700
  continue;
424
701
  }
425
702
  flush();
703
+ boundary(true);
426
704
  current = heading ? {
427
705
  kind: 'heading',
428
706
  level: Number(heading[1]),
@@ -436,10 +714,14 @@ function removeElements(html, tags) {
436
714
  if (BREAK_TAGS.has(tag)) {
437
715
  if (tag === 'br' && current) {
438
716
  buffer += ' ';
717
+ if (anchor) anchor.text += ' ';
439
718
  continue;
440
719
  }
441
720
  flush();
721
+ boundary(true);
722
+ continue;
442
723
  }
724
+ if (RUN_BOUNDARY.has(tag)) boundary(false);
443
725
  }
444
726
  flush();
445
727
  return {
@@ -447,6 +729,11 @@ function removeElements(html, tags) {
447
729
  description,
448
730
  lang,
449
731
  canonical,
732
+ ogImage,
733
+ siteName,
734
+ icons,
735
+ brand,
736
+ organization,
450
737
  blocks,
451
738
  images,
452
739
  navLinks,