@buildinternet/uploads 0.37.0 → 0.37.1
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/dist/client.d.ts +1 -1
- package/dist/commands.d.ts +17 -2
- package/dist/commands.js +18 -3
- package/dist/github.d.ts +18 -10
- package/dist/github.js +101 -68
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -243,7 +243,7 @@ export interface FindGalleriesByReferenceOptions {
|
|
|
243
243
|
* anyway, defeating the point of the server-side gate, so the CLI surfaces
|
|
244
244
|
* the decline instead.
|
|
245
245
|
*/
|
|
246
|
-
export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "not_authorized" | "unavailable";
|
|
246
|
+
export type GithubCommentDeclineReason = "app_unconfigured" | "not_installed" | "forbidden" | "not_authorized" | "actor_not_authorized" | "unavailable";
|
|
247
247
|
export type GithubCommentResult = {
|
|
248
248
|
posted: true;
|
|
249
249
|
action: "created" | "updated" | "skipped";
|
package/dist/commands.d.ts
CHANGED
|
@@ -165,8 +165,10 @@ export interface AttachmentsCommentResult {
|
|
|
165
165
|
export declare function commentViaSuffix(via: AttachmentsCommentResult["via"]): string;
|
|
166
166
|
/**
|
|
167
167
|
* Thrown by `syncAttachmentsComment` when the server declines with
|
|
168
|
-
* `not_authorized` (issue #297 baseline control
|
|
169
|
-
* different workspace
|
|
168
|
+
* `not_authorized` (issue #297 baseline control — this repo is bound to a
|
|
169
|
+
* different workspace) or `actor_not_authorized` (issue #297 control 2 — the
|
|
170
|
+
* workspace requires the caller to be on the target PR/issue thread).
|
|
171
|
+
* Deliberately not caught by the generic "bot endpoint
|
|
170
172
|
* unreachable" fallback below: falling back to gh here would let the
|
|
171
173
|
* human's own credentials post anyway, defeating the point of the
|
|
172
174
|
* server-side gate.
|
|
@@ -490,6 +492,19 @@ export interface DoctorReport {
|
|
|
490
492
|
scopes?: string[];
|
|
491
493
|
/** Workspace/token mismatch warning (also present in hints). */
|
|
492
494
|
warning?: string;
|
|
495
|
+
/**
|
|
496
|
+
* Bring-your-own-bucket storage status (issue #583 Phase 3). `GET
|
|
497
|
+
* /me/workspaces/:name/storage` is session-gated (Better Auth cookie or
|
|
498
|
+
* bearer via the AUTH service — see `session-auth.ts`); the CLI only ever
|
|
499
|
+
* holds a minted `up_<workspace>_…` workspace token, never a session
|
|
500
|
+
* bearer, so doctor cannot reach that route today. Until a token-authed
|
|
501
|
+
* read path exists, this is an honest "can't check from here" rather than
|
|
502
|
+
* a fabricated mode.
|
|
503
|
+
*/
|
|
504
|
+
storage: {
|
|
505
|
+
checked: false;
|
|
506
|
+
note: string;
|
|
507
|
+
};
|
|
493
508
|
hints: string[];
|
|
494
509
|
/** `screenshot`'s local-browser detection (fs scans only — never launches a browser). */
|
|
495
510
|
browser: {
|
package/dist/commands.js
CHANGED
|
@@ -447,8 +447,10 @@ export function commentViaSuffix(via) {
|
|
|
447
447
|
}
|
|
448
448
|
/**
|
|
449
449
|
* Thrown by `syncAttachmentsComment` when the server declines with
|
|
450
|
-
* `not_authorized` (issue #297 baseline control
|
|
451
|
-
* different workspace
|
|
450
|
+
* `not_authorized` (issue #297 baseline control — this repo is bound to a
|
|
451
|
+
* different workspace) or `actor_not_authorized` (issue #297 control 2 — the
|
|
452
|
+
* workspace requires the caller to be on the target PR/issue thread).
|
|
453
|
+
* Deliberately not caught by the generic "bot endpoint
|
|
452
454
|
* unreachable" fallback below: falling back to gh here would let the
|
|
453
455
|
* human's own credentials post anyway, defeating the point of the
|
|
454
456
|
* server-side gate.
|
|
@@ -485,6 +487,14 @@ export async function syncAttachmentsComment(client, target, run, workspace, opt
|
|
|
485
487
|
`Run \`uploads github link --status --repo ${target.repo}\` to see who owns the ` +
|
|
486
488
|
`binding, use that workspace instead, or post the comment manually with gh.`);
|
|
487
489
|
}
|
|
490
|
+
// Actor-on-PR gate (issue #297 control 2, workspace opt-in): same
|
|
491
|
+
// no-gh-fallback rule as not_authorized — the workspace explicitly asked
|
|
492
|
+
// the server to hold this line, so the CLI shouldn't route around it.
|
|
493
|
+
if (bot.reason === "actor_not_authorized") {
|
|
494
|
+
throw new GithubCommentAuthorizationError(`${bot.message ?? `You are not an actor on ${target.repo}#${target.num}.`}\n` +
|
|
495
|
+
`Ask an authorized thread participant to run this, or post the ` +
|
|
496
|
+
`comment manually with gh.`);
|
|
497
|
+
}
|
|
488
498
|
// Installed-but-unapproved is a fixable misconfiguration, not a silent
|
|
489
499
|
// degrade: tell the user (and how to fix it) before falling back to gh.
|
|
490
500
|
if (bot.reason === "forbidden" && bot.message) {
|
|
@@ -1384,7 +1394,7 @@ export function mergeStagingMeta(base, target) {
|
|
|
1384
1394
|
* for both the human-mode stderr line and the JSON `hint` field.
|
|
1385
1395
|
*/
|
|
1386
1396
|
export function putStagingNoteText(branch) {
|
|
1387
|
-
return (`note: staged for branch ${branch} — auto-
|
|
1397
|
+
return (`note: staged for branch ${branch} — auto-comments to pull request when opened ` +
|
|
1388
1398
|
`(or run: uploads attach --promote once it exists). Use --ref/--prefix for a plain dated upload.`);
|
|
1389
1399
|
}
|
|
1390
1400
|
/**
|
|
@@ -2921,6 +2931,10 @@ export async function buildDoctorReport(config, client, detectRoots) {
|
|
|
2921
2931
|
usage,
|
|
2922
2932
|
scopes,
|
|
2923
2933
|
warning: mismatch,
|
|
2934
|
+
storage: {
|
|
2935
|
+
checked: false,
|
|
2936
|
+
note: "not checked from the CLI — storage settings (shared vs. bring-your-own-bucket) live behind a signed-in session; sign in on the web (Account → workspace → Settings) to view mode and verification status",
|
|
2937
|
+
},
|
|
2924
2938
|
hints,
|
|
2925
2939
|
browser,
|
|
2926
2940
|
};
|
|
@@ -2957,6 +2971,7 @@ export async function runDoctor(ctx, args, help = false) {
|
|
|
2957
2971
|
else {
|
|
2958
2972
|
lines.push(`browser: ${report.browser.note ?? "not supported in this runtime"}`);
|
|
2959
2973
|
}
|
|
2974
|
+
lines.push(`storage: ${report.storage.note}`);
|
|
2960
2975
|
if (report.warning)
|
|
2961
2976
|
lines.push(`warning: ${report.warning}`);
|
|
2962
2977
|
for (const h of report.hints)
|
package/dist/github.d.ts
CHANGED
|
@@ -76,8 +76,8 @@ export declare function attachmentsMarker(workspace?: string): string;
|
|
|
76
76
|
export declare const MAX_INLINE_ATTACHMENT_IMAGES = 16;
|
|
77
77
|
/**
|
|
78
78
|
* Per-render knobs for the managed comment (issue #307), sourced from repo
|
|
79
|
-
* comment config. `imageWidth: "auto"`
|
|
80
|
-
*
|
|
79
|
+
* comment config. `imageWidth: "auto"` uses per-item filename heuristics plus
|
|
80
|
+
* density-aware sizing (solo/sparse/dense from the inlined count); `"full"`
|
|
81
81
|
* omits the `width` attribute entirely; a number overrides every width site.
|
|
82
82
|
*/
|
|
83
83
|
export interface CommentRenderOptions {
|
|
@@ -134,21 +134,29 @@ export interface GalleryCommentItem {
|
|
|
134
134
|
itemUrl?: string;
|
|
135
135
|
}[];
|
|
136
136
|
}
|
|
137
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* How crowded the managed comment is. Sparse comments (one shot, a single
|
|
139
|
+
* before/after) get larger embeds; dense comments keep compact historical sizes.
|
|
140
|
+
*/
|
|
141
|
+
export type AttachmentDensity = "solo" | "sparse" | "dense";
|
|
142
|
+
/** Dense (historical) default max width for images in the managed comment. */
|
|
138
143
|
export declare const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
|
|
139
144
|
/** Portrait / device mockups — keep phones readable, not full-column. */
|
|
140
145
|
export declare const ATTACHMENT_IMAGE_WIDTH_PORTRAIT = 280;
|
|
141
146
|
/** Wide UI / browser chrome. */
|
|
142
147
|
export declare const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
|
|
148
|
+
/** Dense pair-cell cap (side-by-side before/after). */
|
|
149
|
+
export declare const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
150
|
+
/** Map an inlined-media count onto a density tier. */
|
|
151
|
+
export declare function attachmentDensityForCount(inlinedCount: number): AttachmentDensity;
|
|
152
|
+
/** Pair-cell cap for the given density. */
|
|
153
|
+
export declare function attachmentPairWidth(density?: AttachmentDensity): number;
|
|
143
154
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
155
|
+
* Display width for a GitHub comment embed. Filenames are a weak but practical
|
|
156
|
+
* signal (we don't re-fetch dimensions when rebuilding the comment). `density`
|
|
157
|
+
* only affects managed-comment auto layout; other callers leave it `"dense"`.
|
|
146
158
|
*/
|
|
147
|
-
export declare function attachmentImageWidth(filename: string): number;
|
|
148
|
-
/** Max display width for one image inside a before/after pair row — smaller
|
|
149
|
-
* than a standalone image so two side by side stay under GitHub's comment
|
|
150
|
-
* column width (and don't overflow on mobile). */
|
|
151
|
-
export declare const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
159
|
+
export declare function attachmentImageWidth(filename: string, density?: AttachmentDensity): number;
|
|
152
160
|
/**
|
|
153
161
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
154
162
|
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
package/dist/github.js
CHANGED
|
@@ -171,26 +171,52 @@ export const AUTO_RENDER_OPTIONS = {
|
|
|
171
171
|
metaState: true,
|
|
172
172
|
note: null,
|
|
173
173
|
};
|
|
174
|
-
/**
|
|
174
|
+
/** Dense (historical) default max width for images in the managed comment. */
|
|
175
175
|
export const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
|
|
176
176
|
/** Portrait / device mockups — keep phones readable, not full-column. */
|
|
177
177
|
export const ATTACHMENT_IMAGE_WIDTH_PORTRAIT = 280;
|
|
178
178
|
/** Wide UI / browser chrome. */
|
|
179
179
|
export const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
|
|
180
|
+
/** Dense pair-cell cap (side-by-side before/after). */
|
|
181
|
+
export const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
182
|
+
/** Per-density widths for `imageWidth: "auto"`. Dense reuses the exports above. */
|
|
183
|
+
const WIDTH_BY_DENSITY = {
|
|
184
|
+
solo: { default: 720, portrait: 360, wide: 800, pair: 400 },
|
|
185
|
+
sparse: { default: 560, portrait: 300, wide: 720, pair: 380 },
|
|
186
|
+
dense: {
|
|
187
|
+
default: ATTACHMENT_IMAGE_WIDTH_DEFAULT,
|
|
188
|
+
portrait: ATTACHMENT_IMAGE_WIDTH_PORTRAIT,
|
|
189
|
+
wide: ATTACHMENT_IMAGE_WIDTH_WIDE,
|
|
190
|
+
pair: ATTACHMENT_IMAGE_WIDTH_PAIR,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
/** Map an inlined-media count onto a density tier. */
|
|
194
|
+
export function attachmentDensityForCount(inlinedCount) {
|
|
195
|
+
if (inlinedCount <= 1)
|
|
196
|
+
return "solo";
|
|
197
|
+
if (inlinedCount <= 3)
|
|
198
|
+
return "sparse";
|
|
199
|
+
return "dense";
|
|
200
|
+
}
|
|
201
|
+
/** Pair-cell cap for the given density. */
|
|
202
|
+
export function attachmentPairWidth(density = "dense") {
|
|
203
|
+
return WIDTH_BY_DENSITY[density].pair;
|
|
204
|
+
}
|
|
180
205
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
206
|
+
* Display width for a GitHub comment embed. Filenames are a weak but practical
|
|
207
|
+
* signal (we don't re-fetch dimensions when rebuilding the comment). `density`
|
|
208
|
+
* only affects managed-comment auto layout; other callers leave it `"dense"`.
|
|
183
209
|
*/
|
|
184
|
-
export function attachmentImageWidth(filename) {
|
|
210
|
+
export function attachmentImageWidth(filename, density = "dense") {
|
|
211
|
+
const table = WIDTH_BY_DENSITY[density];
|
|
185
212
|
const n = filename.toLowerCase();
|
|
186
|
-
if (/(?:^|[-_.])(browser|desktop|dashboard|wide)(?:[-_.]|$)/.test(n))
|
|
187
|
-
return
|
|
188
|
-
}
|
|
213
|
+
if (/(?:^|[-_.])(browser|desktop|dashboard|wide)(?:[-_.]|$)/.test(n))
|
|
214
|
+
return table.wide;
|
|
189
215
|
if (/(?:^|[-_.])(phone|iphone|ipad|pixel|android|mobile|device)(?:[-_.]|$)/.test(n) ||
|
|
190
216
|
/iphone|pixel-?\d/.test(n)) {
|
|
191
|
-
return
|
|
217
|
+
return table.portrait;
|
|
192
218
|
}
|
|
193
|
-
return
|
|
219
|
+
return table.default;
|
|
194
220
|
}
|
|
195
221
|
/** `m:ss` under an hour, `h:mm:ss` at or above one. */
|
|
196
222
|
function formatDuration(seconds) {
|
|
@@ -205,19 +231,16 @@ function formatDuration(seconds) {
|
|
|
205
231
|
}
|
|
206
232
|
/**
|
|
207
233
|
* Display width for a video poster. Real dimensions only *select* among the
|
|
208
|
-
*
|
|
209
|
-
* result is capped at the real width so a small clip is never upscaled.
|
|
234
|
+
* density table's tiers — a raw 1920 would blow out the comment column — and
|
|
235
|
+
* the result is capped at the real width so a small clip is never upscaled.
|
|
210
236
|
*/
|
|
211
|
-
function posterImageWidth(videoMeta, filename) {
|
|
237
|
+
function posterImageWidth(videoMeta, filename, density = "dense") {
|
|
212
238
|
const w = videoMeta?.width ?? 0;
|
|
213
239
|
const h = videoMeta?.height ?? 0;
|
|
214
240
|
if (w <= 0 || h <= 0)
|
|
215
|
-
return attachmentImageWidth(filename);
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
: w / h >= 16 / 9
|
|
219
|
-
? ATTACHMENT_IMAGE_WIDTH_WIDE
|
|
220
|
-
: ATTACHMENT_IMAGE_WIDTH_DEFAULT;
|
|
241
|
+
return attachmentImageWidth(filename, density);
|
|
242
|
+
const table = WIDTH_BY_DENSITY[density];
|
|
243
|
+
const chosen = h > w ? table.portrait : w / h >= 16 / 9 ? table.wide : table.default;
|
|
221
244
|
return Math.min(chosen, w);
|
|
222
245
|
}
|
|
223
246
|
function escapeHtmlAttr(s) {
|
|
@@ -236,45 +259,37 @@ function escapeMarkdownText(s) {
|
|
|
236
259
|
return s.replace(/([\\`*_[\]~])/g, "\\$1");
|
|
237
260
|
}
|
|
238
261
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* metadata stays byte-identical to the pre-#365 render.
|
|
242
|
-
*
|
|
243
|
-
* Neither value is pre-sanitized: metadata values are printable ASCII up to
|
|
244
|
-
* 512 chars, and while the CLI validates `--state` against a closed enum,
|
|
245
|
-
* `PATCH /v1/:workspace/files/:key` can set any valid metadata value. A
|
|
246
|
-
* whitespace-only value passes that validation (length-1 printable ASCII), so
|
|
247
|
-
* treat it as absent rather than rendering a dangling separator.
|
|
248
|
-
*
|
|
249
|
-
* Bare `/` is stored/searchable but omitted from captions (issue #375) —
|
|
250
|
-
* alone it is a stray character, and as a prefix next to `state` it is
|
|
251
|
-
* noise. Only exact `/` after trim is suppressed.
|
|
262
|
+
* Collect path then state for a caption (issue #365). Bare `/` and
|
|
263
|
+
* whitespace-only values are omitted (issue #375). Empty when nothing usable.
|
|
252
264
|
*/
|
|
253
|
-
function
|
|
254
|
-
const
|
|
265
|
+
function metaCaptionValues(meta, options) {
|
|
266
|
+
const values = [];
|
|
255
267
|
const path = meta?.path?.trim();
|
|
256
268
|
if (options.metaPath && path && path !== "/")
|
|
257
|
-
|
|
269
|
+
values.push(path);
|
|
258
270
|
const state = meta?.state?.trim();
|
|
259
271
|
if (options.metaState && state)
|
|
260
|
-
|
|
261
|
-
return
|
|
262
|
-
}
|
|
263
|
-
/** `<sub>` caption body for an inline image, or null when there is nothing to say. */
|
|
264
|
-
function metaCaptionHtml(meta, options) {
|
|
265
|
-
const parts = metaCaptionParts(meta, options);
|
|
266
|
-
return parts.length > 0 ? parts.map(escapeHtmlText).join(" · ") : null;
|
|
272
|
+
values.push(state);
|
|
273
|
+
return values;
|
|
267
274
|
}
|
|
268
275
|
/**
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
276
|
+
* Format path/state as code tokens. HTML → `<code>…</code>`; markdown →
|
|
277
|
+
* `` `…` `` (backslash-escape if the value itself contains a backtick).
|
|
278
|
+
* Returns `""` when there is nothing to say.
|
|
272
279
|
*/
|
|
273
|
-
function
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
280
|
+
function formatMetaCaption(meta, options, mode) {
|
|
281
|
+
const values = metaCaptionValues(meta, options);
|
|
282
|
+
if (values.length === 0)
|
|
276
283
|
return "";
|
|
277
|
-
|
|
284
|
+
if (mode === "html") {
|
|
285
|
+
return values.map((v) => `<code>${escapeHtmlText(v)}</code>`).join(" · ");
|
|
286
|
+
}
|
|
287
|
+
return values
|
|
288
|
+
.map((v) => {
|
|
289
|
+
const esc = escapeHtmlText(v);
|
|
290
|
+
return esc.includes("`") ? escapeMarkdownText(esc) : `\`${esc}\``;
|
|
291
|
+
})
|
|
292
|
+
.join(" · ");
|
|
278
293
|
}
|
|
279
294
|
/** Resolved pixel width for an image site, or `null` meaning "omit the width
|
|
280
295
|
* attribute". `"auto"` defers to the caller's per-item heuristic (`autoPx`);
|
|
@@ -388,28 +403,37 @@ function pairAttachments(items, isImageAt) {
|
|
|
388
403
|
}
|
|
389
404
|
return { partnerOf, roleOf };
|
|
390
405
|
}
|
|
391
|
-
|
|
392
|
-
* than a standalone image so two side by side stay under GitHub's comment
|
|
393
|
-
* column width (and don't overflow on mobile). */
|
|
394
|
-
export const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
|
|
395
|
-
function renderPairCell(item, label, options) {
|
|
406
|
+
function renderPairCell(item, label, options, density) {
|
|
396
407
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
397
408
|
const src = item.embedUrl ?? item.url;
|
|
398
409
|
const link = item.pageUrl ?? item.url;
|
|
399
|
-
const autoPx = Math.min(attachmentImageWidth(name),
|
|
410
|
+
const autoPx = Math.min(attachmentImageWidth(name, density), attachmentPairWidth(density));
|
|
400
411
|
const w = resolvedWidth(autoPx, options);
|
|
401
412
|
const alt = escapeHtmlAttr(name);
|
|
402
413
|
const href = escapeHtmlAttr((link ?? src));
|
|
403
414
|
const imgSrc = escapeHtmlAttr(src);
|
|
404
|
-
const caption =
|
|
415
|
+
const caption = formatMetaCaption(item.meta, options, "html");
|
|
405
416
|
const captionHtml = caption ? `<br><sub>${caption}</sub>` : "";
|
|
406
417
|
return `<td align="center"><sub><strong>${label}</strong></sub><br><a href="${href}">${imgTag(w, alt, imgSrc)}</a>${captionHtml}</td>`;
|
|
407
418
|
}
|
|
408
|
-
/** One side-by-side before/after row (issue #419)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
419
|
+
/** One side-by-side before/after row (issue #419). */
|
|
420
|
+
function renderPairRow(beforeItem, afterItem, options, density) {
|
|
421
|
+
return `<table><tr>${renderPairCell(beforeItem, "Before", options, density)}${renderPairCell(afterItem, "After", options, density)}</tr></table>`;
|
|
422
|
+
}
|
|
423
|
+
/** How many image/poster items will fit under `maxInlineImages` (for density). */
|
|
424
|
+
function countInlinableMedia(sorted, maxInlineImages) {
|
|
425
|
+
let count = 0;
|
|
426
|
+
for (const item of sorted) {
|
|
427
|
+
if (count >= maxInlineImages)
|
|
428
|
+
break;
|
|
429
|
+
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
430
|
+
const src = item.embedUrl ?? item.url;
|
|
431
|
+
const isImage = Boolean(src) && inferContentType(name).startsWith("image/");
|
|
432
|
+
const isPoster = Boolean(item.posterUrl) && inferContentType(name).startsWith("video/");
|
|
433
|
+
if (isImage || isPoster)
|
|
434
|
+
count++;
|
|
435
|
+
}
|
|
436
|
+
return count;
|
|
413
437
|
}
|
|
414
438
|
/**
|
|
415
439
|
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
@@ -444,6 +468,10 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
444
468
|
return Boolean(src) && inferContentType(name).startsWith("image/");
|
|
445
469
|
});
|
|
446
470
|
const { partnerOf, roleOf } = pairAttachments(sorted, isImageAt);
|
|
471
|
+
// One screenshot → large; a wall of shots → compact historical sizes.
|
|
472
|
+
const density = options.imageWidth === "auto"
|
|
473
|
+
? attachmentDensityForCount(countInlinableMedia(sorted, options.maxInlineImages))
|
|
474
|
+
: "dense";
|
|
447
475
|
const consumedByPair = new Set();
|
|
448
476
|
let inlinedImages = 0;
|
|
449
477
|
const overflowImages = [];
|
|
@@ -459,7 +487,7 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
459
487
|
consumedByPair.add(partnerIdx);
|
|
460
488
|
const beforeItem = roleOf.get(idx) === "before" ? item : partner;
|
|
461
489
|
const afterItem = roleOf.get(idx) === "before" ? partner : item;
|
|
462
|
-
lines.push(renderPairRow(beforeItem, afterItem, options), "");
|
|
490
|
+
lines.push(renderPairRow(beforeItem, afterItem, options, density), "");
|
|
463
491
|
continue;
|
|
464
492
|
}
|
|
465
493
|
// Cap already full for a two-image row — degrade this pair to two
|
|
@@ -483,7 +511,7 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
483
511
|
}
|
|
484
512
|
if (isPosterVideo) {
|
|
485
513
|
inlinedImages++;
|
|
486
|
-
const autoPx = posterImageWidth(item.videoMeta, name);
|
|
514
|
+
const autoPx = posterImageWidth(item.videoMeta, name, density);
|
|
487
515
|
const w = resolvedWidth(autoPx, options);
|
|
488
516
|
const href = escapeHtmlAttr(link ?? item.posterUrl);
|
|
489
517
|
lines.push(`<a href="${href}">${imgTag(w, escapeHtmlAttr(name), escapeHtmlAttr(item.posterUrl))}</a>`);
|
|
@@ -493,29 +521,33 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
493
521
|
if (item.videoMeta?.durationSeconds != null) {
|
|
494
522
|
parts.push(formatDuration(item.videoMeta.durationSeconds));
|
|
495
523
|
}
|
|
496
|
-
|
|
524
|
+
const metaCap = formatMetaCaption(item.meta, options, "html");
|
|
525
|
+
if (metaCap)
|
|
526
|
+
parts.push(metaCap);
|
|
497
527
|
lines.push(`<sub>${parts.join(" · ")}</sub>`, "");
|
|
498
528
|
}
|
|
499
529
|
else if (isImage) {
|
|
500
530
|
inlinedImages++;
|
|
501
531
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
502
532
|
// img src uses embed host when available (Camo revalidates); click-through prefers the file page.
|
|
503
|
-
const autoPx = attachmentImageWidth(name);
|
|
533
|
+
const autoPx = attachmentImageWidth(name, density);
|
|
504
534
|
const w = resolvedWidth(autoPx, options);
|
|
505
535
|
const alt = escapeHtmlAttr(name);
|
|
506
536
|
const href = escapeHtmlAttr(link ?? src);
|
|
507
537
|
const imgSrc = escapeHtmlAttr(src);
|
|
508
538
|
lines.push(`<a href="${href}">${imgTag(w, alt, imgSrc)}</a>`);
|
|
509
|
-
const caption =
|
|
539
|
+
const caption = formatMetaCaption(item.meta, options, "html");
|
|
510
540
|
if (caption)
|
|
511
541
|
lines.push(`<sub>${caption}</sub>`);
|
|
512
542
|
lines.push("");
|
|
513
543
|
}
|
|
514
544
|
else if (link) {
|
|
515
|
-
|
|
545
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
546
|
+
lines.push(`- [${name}](${link})${cap ? ` · ${cap}` : ""}`);
|
|
516
547
|
}
|
|
517
548
|
else {
|
|
518
|
-
|
|
549
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
550
|
+
lines.push(`- ${name}${cap ? ` · ${cap}` : ""}`);
|
|
519
551
|
}
|
|
520
552
|
}
|
|
521
553
|
if (overflowImages.length > 0) {
|
|
@@ -524,7 +556,8 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
524
556
|
for (const item of overflowImages) {
|
|
525
557
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
526
558
|
const link = item.pageUrl ?? item.url;
|
|
527
|
-
const
|
|
559
|
+
const cap = formatMetaCaption(item.meta, options, "markdown");
|
|
560
|
+
const suffix = cap ? ` · ${cap}` : "";
|
|
528
561
|
lines.push(link ? `- [${name}](${link})${suffix}` : `- ${name}${suffix}`);
|
|
529
562
|
}
|
|
530
563
|
lines.push("", "</details>", "");
|