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.
- package/README.md +4 -1
- package/dist/cli.js +107 -16
- 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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|