droppage 0.2.1 → 0.2.2

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 (3) hide show
  1. package/README.md +4 -1
  2. package/dist/cli.js +107 -16
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,7 @@ Open the printed link in any browser. That's it.
49
49
  | `droppage recipients resend <slug> <email>` | Queue a fresh invitation for an existing recipient. |
50
50
  | `droppage image upload <files...> --page <slug>` | Advanced: add images (PNG, JPEG, GIF, WebP, AVIF, SVG; up to 4 MB each) to an existing page and print their URLs. `--json` prints `[{file,id,url,bytes,content_type,existing}]`. |
51
51
  | `droppage image list [--page <slug>]` | List your pages' images; with `--page`, one page and its image budget. `--json` for machines. |
52
- | `droppage image delete <id-or-url...>` | Delete images. Cached copies may still show for up to a day. |
52
+ | `droppage image delete <id-or-url...>` | Delete images. They stop being served within 5 minutes; a browser that already loaded one may keep it up to a day. |
53
53
  | `droppage quota` | Show the current plan, page usage, monthly meter, reset date, and enabled features. `--json` returns the account API body. |
54
54
  | `droppage feedback <message>` | Send feedback without requiring login. Options: `--kind idea|bug|review|other`, `--agent [model]`, `--contact <email>`, review-only `--rating <1-5>` and `--allow-quote`. Quote permission never publishes automatically. |
55
55
  | `droppage upgrade` | Install the latest CLI globally with npm and report the installed version. |
@@ -104,6 +104,9 @@ only the page URL.
104
104
 
105
105
  - `update` uploads any new local images, keeps the same URL for an unchanged image, and deletes
106
106
  the page's images the new HTML no longer uses.
107
+ - Besides `<img src>`, the CLI picks up local images in `srcset` on `<img>`/`<source>` (every
108
+ candidate, descriptors kept), `<link rel="icon"|"apple-touch-icon" href>`, and `url(...)` inside
109
+ `<style>` blocks (0.2.2+). Not picked up: `style="..."` attributes and `.ico` files.
107
110
  - Deleting a page, or the page expiring, removes its images too. Images never outlive their page.
108
111
  - Image budget per page by plan: Free 5 MB, Starter 20 MB, Builder 50 MB. Each image up to 4 MB.
109
112
  PNG, JPEG, GIF, WebP, AVIF, SVG. Image traffic counts toward your monthly bandwidth.
package/dist/cli.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "droppage",
18
- version: "0.2.1",
18
+ version: "0.2.2",
19
19
  description: "Publish HTML files as shareable, access-controlled live pages",
20
20
  type: "module",
21
21
  license: "Apache-2.0",
@@ -521,6 +521,76 @@ var init_duration = __esm({
521
521
  // src/lib/html-images.ts
522
522
  import { existsSync, realpathSync, statSync } from "fs";
523
523
  import { dirname, extname, relative, resolve, sep } from "path";
524
+ function imageSlots(html) {
525
+ const slots = [];
526
+ const attrValue = (match) => ({ start: match.index + match[1].length + 1, value: match[3] });
527
+ for (const match of html.matchAll(SRC_RE)) {
528
+ const { start, value } = attrValue(match);
529
+ slots.push({ start, end: start + value.length, ref: value.trim() });
530
+ }
531
+ for (const match of html.matchAll(SRCSET_RE)) {
532
+ const { start, value } = attrValue(match);
533
+ for (const url of srcsetUrls(value)) slots.push({ start: start + url.offset, end: start + url.offset + url.ref.length, ref: url.ref });
534
+ }
535
+ for (const match of html.matchAll(LINK_RE)) {
536
+ const tag = match[0];
537
+ let rel = null;
538
+ let href = null;
539
+ for (const attr of tag.matchAll(ATTR_RE)) {
540
+ const name = attr[1].toLowerCase();
541
+ const value = attr[3] ?? attr[4];
542
+ const end = attr.index + attr[0].length - (attr[3] === void 0 ? 0 : 1);
543
+ if (name === "rel") rel = value;
544
+ if (name === "href") href = { offset: end - value.length, value };
545
+ }
546
+ if (!rel || !href || !rel.toLowerCase().split(/\s+/).some((token) => ICON_RELS.has(token))) continue;
547
+ const start = match.index + href.offset;
548
+ slots.push({ start, end: start + href.value.length, ref: href.value.trim() });
549
+ }
550
+ for (const match of html.matchAll(STYLE_RE)) {
551
+ const cssStart = match.index + match[1].length;
552
+ const css = match[2].replace(/\/\*[\s\S]*?\*\//g, (comment) => " ".repeat(comment.length));
553
+ for (const url of css.matchAll(CSS_URL_RE)) {
554
+ const value = url[1] ?? url[2] ?? url[3];
555
+ const quoted = url[1] !== void 0 || url[2] !== void 0;
556
+ const paren = url[0].indexOf("(") + 1;
557
+ const open2 = paren + url[0].slice(paren).search(/\S/);
558
+ const start = cssStart + url.index + open2 + (quoted ? 1 : 0);
559
+ slots.push({ start, end: start + value.length, ref: value.trim() });
560
+ }
561
+ }
562
+ slots.sort((a, b) => a.start - b.start);
563
+ const ordered = [];
564
+ for (const slot of slots) {
565
+ if (slot.ref && (ordered.length === 0 || slot.start >= ordered[ordered.length - 1].end)) ordered.push(slot);
566
+ }
567
+ return ordered;
568
+ }
569
+ function srcsetUrls(value) {
570
+ const urls = [];
571
+ let i = 0;
572
+ while (i < value.length) {
573
+ while (i < value.length && (/\s/.test(value[i]) || value[i] === ",")) i++;
574
+ if (i >= value.length) break;
575
+ const start = i;
576
+ while (i < value.length && !/\s/.test(value[i])) i++;
577
+ const raw = value.slice(start, i);
578
+ const ref = raw.replace(/,+$/, "");
579
+ if (ref) urls.push({ offset: start, ref });
580
+ if (ref.length !== raw.length) continue;
581
+ let depth = 0;
582
+ for (; i < value.length; i++) {
583
+ const c = value[i];
584
+ if (c === "(") depth++;
585
+ else if (c === ")") depth = Math.max(0, depth - 1);
586
+ else if (c === "," && depth === 0) {
587
+ i++;
588
+ break;
589
+ }
590
+ }
591
+ }
592
+ return urls;
593
+ }
524
594
  function inside(base, path) {
525
595
  const rel = relative(base, path);
526
596
  return !!rel && !rel.startsWith("..") && !rel.startsWith(sep) && !/^[a-z]:/i.test(rel);
@@ -534,8 +604,7 @@ function findLocalImages(html, htmlFile) {
534
604
  return [];
535
605
  }
536
606
  const seen = /* @__PURE__ */ new Map();
537
- for (const match of html.matchAll(SRC_RE)) {
538
- const ref = match[3].trim();
607
+ for (const { ref } of imageSlots(html)) {
539
608
  if (seen.has(ref) || /^(?:[a-z][a-z0-9+.-]*:|\/|\\|#)/i.test(ref)) continue;
540
609
  let decoded;
541
610
  try {
@@ -558,17 +627,29 @@ function findLocalImages(html, htmlFile) {
558
627
  return [...seen.values()];
559
628
  }
560
629
  function rewriteImageRefs(html, urls) {
561
- return html.replace(SRC_RE, (whole, before, quote, ref) => {
562
- const url = urls.get(ref.trim());
563
- return url ? `${before}${quote}${url}${quote}` : whole;
564
- });
630
+ let out = "";
631
+ let last = 0;
632
+ for (const slot of imageSlots(html)) {
633
+ const url = urls.get(slot.ref);
634
+ if (!url) continue;
635
+ out += html.slice(last, slot.start) + url;
636
+ last = slot.end;
637
+ }
638
+ return out + html.slice(last);
565
639
  }
566
- var IMAGE_EXTS, SRC_RE;
640
+ var IMAGE_EXTS, imgAttrRe, SRC_RE, SRCSET_RE, LINK_RE, ATTR_RE, ICON_RELS, STYLE_RE, CSS_URL_RE;
567
641
  var init_html_images = __esm({
568
642
  "src/lib/html-images.ts"() {
569
643
  "use strict";
570
644
  IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg"]);
571
- SRC_RE = /(<(?:img|source)\b[^>]*?\ssrc\s*=\s*)(["'])([^"']+)\2/gi;
645
+ imgAttrRe = (name) => new RegExp(`(<(?:img|source)\\b[^>]*?\\s${name}\\s*=\\s*)(["'])([^"']+)\\2`, "gi");
646
+ SRC_RE = imgAttrRe("src");
647
+ SRCSET_RE = imgAttrRe("srcset");
648
+ LINK_RE = /<link\b[^>]*>/gi;
649
+ ATTR_RE = /\s([a-z][a-z0-9-]*)\s*=\s*(?:(["'])([^"']*)\2|([^\s"'=<>`]+))/gi;
650
+ ICON_RELS = /* @__PURE__ */ new Set(["icon", "apple-touch-icon", "apple-touch-icon-precomposed"]);
651
+ STYLE_RE = /(<style\b[^>]*>)([\s\S]*?)<\/style\s*>/gi;
652
+ CSS_URL_RE = /\burl\(\s*(?:"([^"\n]*)"|'([^'\n]*)'|([^\s"'()]+))\s*\)/gi;
572
653
  }
573
654
  });
574
655
 
@@ -589,6 +670,7 @@ var init_share_url = __esm({
589
670
  });
590
671
 
591
672
  // src/services/images.ts
673
+ import { createHash } from "crypto";
592
674
  import { readFile, stat } from "fs/promises";
593
675
  import { basename } from "path";
594
676
  async function accessToken() {
@@ -668,10 +750,11 @@ async function deleteImage(idOrUrl) {
668
750
  });
669
751
  return id;
670
752
  }
671
- async function pruneImages(slug) {
753
+ async function pruneImages(slug, html) {
672
754
  const body = await call(`/api/pages/${encodeURIComponent(slug)}/images/prune`, {
673
755
  method: "POST",
674
- headers: { Authorization: `Bearer ${await accessToken()}` }
756
+ headers: { Authorization: `Bearer ${await accessToken()}`, "Content-Type": "application/json" },
757
+ body: JSON.stringify(html ? { html_sha256: createHash("sha256").update(html).digest("hex") } : {})
675
758
  });
676
759
  return Number(body?.removed ?? 0);
677
760
  }
@@ -714,8 +797,16 @@ async function attachLocalImages(file, slug, { html, update = false } = {}) {
714
797
  async function hasLocalImages(file) {
715
798
  return findLocalImages((await readFile2(file)).toString("utf8"), file).length > 0;
716
799
  }
717
- async function pruneUnusedImages(slug) {
718
- return pruneImages(slug);
800
+ async function pruneUnusedImages(slug, html) {
801
+ try {
802
+ return await pruneImages(slug, html);
803
+ } catch (err) {
804
+ if (err instanceof ImageRequestError && err.status === 409) {
805
+ console.error(`droppage: warning: unused images were kept (${err.message}); the next update removes them`);
806
+ return 0;
807
+ }
808
+ throw err;
809
+ }
719
810
  }
720
811
  var init_page_images = __esm({
721
812
  "src/services/page-images.ts"() {
@@ -1156,7 +1247,7 @@ function registerUpdate(program2) {
1156
1247
  validateHtmlUpload(file, await readFile5(file));
1157
1248
  const { html } = await attachLocalImages(file, slug, { update: true });
1158
1249
  const result = await updatePageHtml({ slug, file, user, html });
1159
- const removed = await pruneUnusedImages(slug);
1250
+ const removed = await pruneUnusedImages(slug, html);
1160
1251
  if (removed) console.error(`droppage: removed ${removed} image(s) the page no longer uses`);
1161
1252
  if (expiresAt !== void 0) {
1162
1253
  await setPageExpiry(slug, expiresAt);
@@ -1871,7 +1962,7 @@ function fail(err) {
1871
1962
  process.exitCode = 1;
1872
1963
  }
1873
1964
  function registerImage(program2) {
1874
- const image = program2.command("image").description("Images that belong to a page (uploaded automatically by upload/update from local <img src> files)");
1965
+ const image = program2.command("image").description("Images that belong to a page (uploaded automatically by upload/update from local files the HTML references)");
1875
1966
  image.command("upload").description("Add images to one of your pages and print their URLs (PNG, JPEG, GIF, WebP, AVIF, SVG; up to 4 MB each)").argument("<files...>", "image files").requiredOption("--page <slug>", "the page the images belong to (they go when it is deleted or expires)").option("--json", "print the results as JSON").action(async (files, options) => {
1876
1967
  if (!await signedIn()) return;
1877
1968
  const results = [];
@@ -1915,7 +2006,7 @@ function registerImage(program2) {
1915
2006
  fail(err);
1916
2007
  }
1917
2008
  });
1918
- image.command("delete").description("Delete images (by id or URL); a browser that already loaded one may keep it up to a day").argument("<images...>", "image ids or URLs").action(async (ids) => {
2009
+ image.command("delete").description("Delete images (by id or URL); they stop being served within 5 minutes; a browser that already loaded one may keep it up to a day").argument("<images...>", "image ids or URLs").action(async (ids) => {
1919
2010
  if (!await signedIn()) return;
1920
2011
  for (const value of ids) {
1921
2012
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "droppage",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Publish HTML files as shareable, access-controlled live pages",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",