pandorga 1.6.8 → 1.6.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 355770a826a6483d7fbc1e014c76fc0ef865d23bba44674cc2153d25ed840a9c
4
- data.tar.gz: 6fd8451064d1a3bc58a8ff4ed53b90606183644756c28984018ff20ae485a543
3
+ metadata.gz: 110f9fdf3636db9ef496093f969e15ef4cd3322ee143c5efadb210fb5e4b98b9
4
+ data.tar.gz: 6c5625b1cbdac652c0120d2b25e49a722c59941862cf91da7f44eff6f1ada004
5
5
  SHA512:
6
- metadata.gz: 331cf7a4687f241ee1b653e2435986bd511536ac3d1bfd21b65c91deef8969994999d8d65696e54a5725e7be41169d10ff9abac70475844a018480869cb19c1c
7
- data.tar.gz: 88fc4c23d1cb5680b054c2e7028e374caab861977114ae235de52547b482de74b0c282957fc5bdf31764c9c3f11e39a65bbd4372ee7ee8fbf397b238b41f1c66
6
+ metadata.gz: 22f67174268b9edd80046d916e6d8394d4ce1df62e8802be7dc4eab8771479825c1ba8b3aa2f668ebef53bc95f116f840ddd300428362b938a19b9955d0914d8
7
+ data.tar.gz: 85ce7605aae925b73d384a9ddbc0269b5b31bb58652b05b60617c5b784f8cfc2c68dc92cc3824ed07bf016f305aee36c5fba17a51c891ecce06cd58a0c3f94d0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.6.9] — 2026-10-10
4
+
5
+ ### Fixed
6
+
7
+ - Home list hydration no longer fills every band in one turn. The project
8
+ specimen measurement yields between projects, each band starts on its
9
+ own turn, and each home section introduction yields before it is
10
+ parsed. Dates reuse one formatter. The lists still render.
11
+
12
+ No `pandorga:` config keys were added or renamed.
13
+
3
14
  ## [1.6.8] — 2026-10-10
4
15
 
5
16
  ### Fixed
@@ -113,15 +113,23 @@
113
113
  return textExcerpt((item && item.body_markdown) || '');
114
114
  }
115
115
 
116
+ var dateFormatters = Object.create(null);
117
+
116
118
  function formatDate(value, locale) {
117
119
  if (!value) return '';
118
120
  var parts = String(value).slice(0, 10).split('-').map(Number);
119
121
  if (parts.length !== 3 || parts.some(isNaN)) return value;
120
- return new Intl.DateTimeFormat(locale || 'en', {
121
- day: '2-digit',
122
- month: 'short',
123
- year: 'numeric'
124
- }).format(new Date(parts[0], parts[1] - 1, parts[2]));
122
+ var key = locale || 'en';
123
+ var formatter = dateFormatters[key];
124
+ if (!formatter) {
125
+ formatter = new Intl.DateTimeFormat(key, {
126
+ day: '2-digit',
127
+ month: 'short',
128
+ year: 'numeric'
129
+ });
130
+ dateFormatters[key] = formatter;
131
+ }
132
+ return formatter.format(new Date(parts[0], parts[1] - 1, parts[2]));
125
133
  }
126
134
 
127
135
  function chronologySortKey(item) {
@@ -353,15 +353,31 @@
353
353
  });
354
354
  }
355
355
 
356
+ function yieldToMain() {
357
+ return new Promise(function (resolve) {
358
+ if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
359
+ scheduler.yield().then(resolve);
360
+ return;
361
+ }
362
+ window.setTimeout(resolve, 0);
363
+ });
364
+ }
365
+
356
366
  function hydrateHomeSections() {
357
367
  var roots = Array.prototype.slice.call(
358
368
  document.querySelectorAll('[data-content-home-section]')
359
369
  );
360
370
  if (!roots.length) return Promise.resolve();
361
371
  return loadPageHeaders().then(function (sections) {
372
+ var chain = Promise.resolve();
362
373
  roots.forEach(function (root) {
363
- applyHomeSection(root, sections[root.getAttribute('data-content-home-section')]);
374
+ chain = chain.then(function () {
375
+ return yieldToMain().then(function () {
376
+ applyHomeSection(root, sections[root.getAttribute('data-content-home-section')]);
377
+ });
378
+ });
364
379
  });
380
+ return chain;
365
381
  }).catch(function (error) {
366
382
  console.error(error);
367
383
  roots.forEach(function (root) { applyHomeSection(root, null); });
@@ -605,21 +621,31 @@
605
621
  }
606
622
 
607
623
  function hydratePageFragments() {
608
- document.querySelectorAll('[data-content-page-header]').forEach(function (el) {
609
- hydratePageHeader(el);
624
+ var headers = Array.prototype.slice.call(document.querySelectorAll('[data-content-page-header]'));
625
+ var chain = Promise.resolve();
626
+ headers.forEach(function (el) {
627
+ chain = chain.then(function () {
628
+ return yieldToMain().then(function () { return hydratePageHeader(el); });
629
+ });
610
630
  });
611
631
  document.querySelectorAll('[data-content-fragment]').forEach(function (el) {
612
632
  var path = el.getAttribute('data-content-fragment');
613
633
  if (!path) return;
614
- mountMarkdownFragment(path, el, {
615
- loadingText: el.getAttribute('data-content-loading') || 'Loading...',
616
- errorText: el.getAttribute('data-content-error') || 'Content could not be loaded right now.'
634
+ chain = chain.then(function () {
635
+ return yieldToMain().then(function () {
636
+ return mountMarkdownFragment(path, el, {
637
+ loadingText: el.getAttribute('data-content-loading') || 'Loading...',
638
+ errorText: el.getAttribute('data-content-error') || 'Content could not be loaded right now.'
639
+ });
640
+ });
617
641
  });
618
642
  });
619
- hydrateHomeSections();
620
- hydrateBandArt();
621
- hydrateContacts();
622
- hydrateHeroPortrait();
643
+ chain.then(function () {
644
+ hydrateHomeSections();
645
+ hydrateBandArt();
646
+ hydrateContacts();
647
+ hydrateHeroPortrait();
648
+ });
623
649
  }
624
650
 
625
651
  function schedulePageFragmentHydration() {
data/_layouts/home.html CHANGED
@@ -121,7 +121,21 @@ layout: shell
121
121
  }
122
122
 
123
123
  function byDateDesc(a, b) {
124
- return String(b.date || '').localeCompare(String(a.date || ''));
124
+ var left = String(b.date || '');
125
+ var right = String(a.date || '');
126
+ if (left > right) return 1;
127
+ if (left < right) return -1;
128
+ return 0;
129
+ }
130
+
131
+ function afterPaint() {
132
+ return new Promise(function (resolve) {
133
+ if (typeof scheduler !== 'undefined' && typeof scheduler.yield === 'function') {
134
+ scheduler.yield().then(resolve);
135
+ return;
136
+ }
137
+ window.setTimeout(resolve, 0);
138
+ });
125
139
  }
126
140
 
127
141
  function tagHrefFrom(element) {
@@ -175,8 +189,13 @@ layout: shell
175
189
  function draftFolios(posts) {
176
190
  var folios = Object.create(null);
177
191
  posts.slice().sort(function (a, b) {
178
- var byDate = String(a.date || '').localeCompare(String(b.date || ''));
179
- return byDate !== 0 ? byDate : String(a.slug || '').localeCompare(String(b.slug || ''));
192
+ var byDate = String(a.date || '') < String(b.date || '') ? -1 : (String(a.date || '') > String(b.date || '') ? 1 : 0);
193
+ if (byDate !== 0) return byDate;
194
+ var left = String(a.slug || '');
195
+ var right = String(b.slug || '');
196
+ if (left < right) return -1;
197
+ if (left > right) return 1;
198
+ return 0;
180
199
  }).forEach(function (entry, index) {
181
200
  folios[entry.slug] = index + 1;
182
201
  });
@@ -221,7 +240,8 @@ layout: shell
221
240
  element.appendChild(fragment);
222
241
  }
223
242
 
224
- function fillBand(section, index) {
243
+ async function fillBand(section, index) {
244
+ await afterPaint();
225
245
  var kind = section.getAttribute('data-home-kind');
226
246
  var collection = section.getAttribute('data-home-collection');
227
247
  var limit = parseInt(section.getAttribute('data-home-limit') || '3', 10) || 3;
@@ -316,7 +336,7 @@ layout: shell
316
336
  fitted(best);
317
337
  }
318
338
 
319
- function paintSpecimen(entry) {
339
+ async function paintSpecimen(entry) {
320
340
  specimenSlot.innerHTML = '';
321
341
  specimenSlot.appendChild(runtime.renderProjectSpecimen(asItem(entry), {
322
342
  href: entry.url || runtime.projectUrl(entry.slug),
@@ -324,10 +344,11 @@ layout: shell
324
344
  folio: positionOf(entry.slug) + 1,
325
345
  openLabel: 'Open project'
326
346
  }));
347
+ await afterPaint();
327
348
  ellipsizeSpecimenCopy(specimenSlot);
328
349
  }
329
350
 
330
- function measureTallestSpecimen() {
351
+ async function measureTallestSpecimen() {
331
352
  var bench = specimenSlot.closest('.home-projects-bench');
332
353
  if (!bench) return 0;
333
354
  var width = specimenSlot.getBoundingClientRect().width;
@@ -348,7 +369,8 @@ layout: shell
348
369
  bench.appendChild(probe);
349
370
 
350
371
  var max = 0;
351
- portfolio.forEach(function (entry, i) {
372
+ for (var i = 0; i < portfolio.length; i += 1) {
373
+ var entry = portfolio[i];
352
374
  probe.innerHTML = '';
353
375
  var card = runtime.renderProjectSpecimen(asItem(entry), {
354
376
  href: entry.url || runtime.projectUrl(entry.slug),
@@ -360,15 +382,16 @@ layout: shell
360
382
  card.style.minHeight = '0';
361
383
  probe.appendChild(card);
362
384
  max = Math.max(max, Math.ceil(card.getBoundingClientRect().height));
363
- });
385
+ await afterPaint();
386
+ }
364
387
  probe.remove();
365
388
  return max;
366
389
  }
367
390
 
368
- function lockSpecimenHeight() {
391
+ async function lockSpecimenHeight() {
369
392
  var bench = specimenSlot.closest('.home-projects-bench');
370
393
  if (!bench) return;
371
- var tallest = measureTallestSpecimen();
394
+ var tallest = await measureTallestSpecimen();
372
395
  /* Floor at the CSS inventory minimum so six rows always clear
373
396
  three stacked resource icons (see --home-inventory-min-height). */
374
397
  var inventoryMin = 0;
@@ -425,8 +448,9 @@ layout: shell
425
448
  });
426
449
  }
427
450
 
428
- lockSpecimenHeight();
429
- paintSpecimen(entryBySlug(selectedSlug));
451
+ await lockSpecimenHeight();
452
+ await paintSpecimen(entryBySlug(selectedSlug));
453
+ await afterPaint();
430
454
  paintInventory();
431
455
 
432
456
  var resizeTimer = null;
@@ -437,8 +461,9 @@ layout: shell
437
461
  var nextWidth = Math.round(specimenSlot.getBoundingClientRect().width);
438
462
  if (nextWidth === lastMeasureWidth) return;
439
463
  lastMeasureWidth = nextWidth;
440
- lockSpecimenHeight();
441
- paintSpecimen(entryBySlug(selectedSlug));
464
+ lockSpecimenHeight().then(function () {
465
+ return paintSpecimen(entryBySlug(selectedSlug));
466
+ });
442
467
  }, 120);
443
468
  });
444
469
  return;
@@ -484,7 +509,11 @@ layout: shell
484
509
 
485
510
  runtime.loadJson('data/writing_index.json').then(function (entries) {
486
511
  var index = Array.isArray(entries) ? entries : [];
487
- sections.forEach(function (section) { fillBand(section, index); });
512
+ var chain = Promise.resolve();
513
+ sections.forEach(function (section) {
514
+ chain = chain.then(function () { return fillBand(section, index); });
515
+ });
516
+ return chain;
488
517
  }).catch(function (error) {
489
518
  console.error(error);
490
519
  sections.forEach(function (section) {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.6.8"
4
+ VERSION = "1.6.9"
5
5
  end
@@ -4,7 +4,7 @@
4
4
  // Lighthouse run treats an empty home as a fast page.
5
5
 
6
6
  import { spawn } from 'node:child_process';
7
- import { mkdtemp, rm } from 'node:fs/promises';
7
+ import { mkdtemp, readFile, rm } from 'node:fs/promises';
8
8
  import { createServer } from 'node:net';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { join } from 'node:path';
@@ -90,17 +90,44 @@ class Cdp {
90
90
  }
91
91
  }
92
92
 
93
- async function waitForDevtools(port) {
94
- for (let attempt = 0; attempt < 50; attempt += 1) {
95
- try {
96
- const response = await fetch(`http://127.0.0.1:${port}/json/version`);
97
- if (response.ok) return response.json();
98
- } catch (_error) {
99
- // Chrome is still starting.
93
+ async function activePort(profile) {
94
+ try {
95
+ const text = await readFile(join(profile, 'DevToolsActivePort'), 'utf8');
96
+ const value = Number(text.split('\n')[0]);
97
+ return Number.isInteger(value) && value > 0 ? value : null;
98
+ } catch (_error) {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ async function waitForDevtools(port, chrome, profile) {
104
+ let stderr = '';
105
+ if (chrome.stderr) {
106
+ chrome.stderr.on('data', (chunk) => {
107
+ stderr += chunk;
108
+ });
109
+ }
110
+ for (let attempt = 0; attempt < 200; attempt += 1) {
111
+ if (chrome.exitCode !== null) {
112
+ throw new Error(`Chrome exited ${chrome.exitCode}: ${stderr.slice(-500)}`);
113
+ }
114
+ const opened = await activePort(profile);
115
+ const candidates = opened && opened !== port ? [port, opened] : [port];
116
+ for (const candidate of candidates) {
117
+ try {
118
+ const response = await fetch(`http://127.0.0.1:${candidate}/json/version`);
119
+ if (response.ok) return response.json();
120
+ } catch (_error) {
121
+ // Chrome is still starting.
122
+ }
100
123
  }
101
124
  await delay(100);
102
125
  }
103
- throw new Error('Chrome DevTools did not answer');
126
+ const opened = await activePort(profile);
127
+ throw new Error(
128
+ `Chrome DevTools did not answer on ${port}` +
129
+ `${opened ? ` (DevToolsActivePort ${opened})` : ''}. ${stderr.slice(-500)}`
130
+ );
104
131
  }
105
132
 
106
133
  async function openSocket(url) {
@@ -155,14 +182,15 @@ async function main() {
155
182
  '--disable-dev-shm-usage',
156
183
  '--no-first-run',
157
184
  '--remote-allow-origins=*',
185
+ '--remote-debugging-address=127.0.0.1',
158
186
  `--remote-debugging-port=${port}`,
159
187
  `--user-data-dir=${profile}`,
160
188
  'about:blank'
161
- ], { stdio: 'ignore' });
189
+ ], { stdio: ['ignore', 'ignore', 'pipe'] });
162
190
 
163
191
  let failed = false;
164
192
  try {
165
- const version = await waitForDevtools(port);
193
+ const version = await waitForDevtools(port, chrome, profile);
166
194
  const ws = await openSocket(version.webSocketDebuggerUrl);
167
195
  const browser = new Cdp(ws);
168
196
  const created = await browser.send('Target.createTarget', { url: 'about:blank' });
@@ -43,7 +43,14 @@ end
43
43
 
44
44
  home = ROOT.join("_layouts/home.html").read
45
45
  fail!("home paint still runs before the runtime") unless home.scan("pandorga:runtime").length >= 2
46
- fail!("home bands still return when the runtime is missing") if home.include?("if (!runtime) return")
46
+ fail!("home bands still fill in one turn") unless home.include?("scheduler.yield") && home.include?("await afterPaint()")
47
+ fail!("specimen measurement does not yield") unless home.include?("await measureTallestSpecimen()")
48
+ fail!("home still fills every band inside one callback") if home.include?("sections.forEach(function (section) { fillBand(section, index); })")
49
+ fragments = ROOT.join("_includes/content-runtime/60-fragments.html").read
50
+ fail!("home section markdown still runs in one turn") unless fragments.include?("function yieldToMain()") &&
51
+ fragments.include?("return yieldToMain().then(function () {")
52
+ listing = ROOT.join("_includes/content-runtime/40-listing.html").read
53
+ fail!("date formatting builds a new formatter per card") unless listing.include?("dateFormatters")
47
54
 
48
55
  marked = ROOT.join("_includes/content-runtime/00-config.html").read
49
56
  fail!("markdown library no longer defers") unless marked.include?("<script defer ")
@@ -67,6 +74,8 @@ fail!("probe ignores the project inventory") unless probe.include?("#home-projec
67
74
  fail!("probe ignores article cards") unless probe.include?("#home-articles .home-featured, #home-articles .ledger-card")
68
75
  fail!("probe ignores the phone viewport") unless probe.include?("name: 'mobile'")
69
76
  fail!("probe ignores the desktop viewport") unless probe.include?("name: 'desktop'")
77
+ fail!("probe does not pin DevTools to IPv4") unless probe.include?("--remote-debugging-address=127.0.0.1")
78
+ fail!("probe gives Chrome only a few seconds to open DevTools") unless probe.include?("attempt < 200")
70
79
 
71
80
  _out, err, status = Open3.capture3("node", "--check", ROOT.join("scripts/check_home_lists.mjs").to_s)
72
81
  fail!("home list probe is not valid JavaScript\n#{err}") unless status.success?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.8
4
+ version: 1.6.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors