fix
All checks were successful
Build Authoring Service / build (push) Successful in 8s

This commit is contained in:
2026-05-13 23:38:27 +01:00
parent 87582d1e34
commit fe0f11535c
2 changed files with 101 additions and 14 deletions

View File

@@ -288,7 +288,7 @@ APP_HTML = r"""<!doctype html>
<button type="button" data-md="link">Link</button>
<button type="button" id="pageLinkBtn">Link to page</button>
<button type="button" id="attachBtn">Insert image</button>
<input id="attachInput" type="file" accept="image/*" />
<input id="attachInput" type="file" accept="image/*,.heic,.heif" />
</div>
<div id="pageLinkPicker" class="link-picker" hidden>
<select id="pageLinkSelect"></select>
@@ -429,16 +429,21 @@ APP_HTML = r"""<!doctype html>
async function api(path, options = {}) {
const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...options });
let data = null;
try {
data = await res.clone().json();
} catch (_err) {
data = { error: await res.text().catch(() => "") };
}
const data = await readApiResponse(res);
if (!res.ok) throw new Error(data.error || "Request failed");
return data;
}
async function readApiResponse(res) {
try {
return await res.clone().json();
} catch (_err) {
const text = await res.text().catch(() => "");
const stripped = text.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
return { error: stripped || `${res.status} ${res.statusText || "Request failed"}` };
}
}
function renderStatus() {
const build = state.build || {};
const latest = build.current || (build.recent && build.recent[build.recent.length - 1]);
@@ -1015,21 +1020,97 @@ APP_HTML = r"""<!doctype html>
pageLinkPicker.hidden = true;
}
const MAX_UPLOAD_BYTES = 750 * 1024;
const MAX_UPLOAD_DIMENSION = 1800;
const MIN_UPLOAD_DIMENSION = 900;
function extensionOf(filename) {
const match = /\.([a-z0-9]+)$/i.exec(filename || "");
return match ? match[1].toLowerCase() : "";
}
function canCompressImage(file) {
const ext = extensionOf(file.name);
const compressibleExt = ["jpg", "jpeg", "png", "webp", "heic", "heif"];
return (file.type.startsWith("image/") || compressibleExt.includes(ext)) && !["gif", "svg"].includes(ext);
}
function loadImage(file) {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
resolve(img);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error("This browser could not read that image format."));
};
img.src = url;
});
}
function canvasToBlob(canvas, quality) {
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("Could not prepare image for upload."));
}, "image/jpeg", quality);
});
}
async function prepareImageUpload(file) {
const ext = extensionOf(file.name);
if (!canCompressImage(file) || (file.size <= MAX_UPLOAD_BYTES && !["heic", "heif"].includes(ext))) {
return file;
}
const img = await loadImage(file);
let maxDimension = MAX_UPLOAD_DIMENSION;
let quality = 0.82;
const stem = (file.name || "image").replace(/\.[^.]+$/, "") || "image";
for (let attempt = 0; attempt < 8; attempt += 1) {
const scale = Math.min(1, maxDimension / Math.max(img.naturalWidth || img.width, img.naturalHeight || img.height));
const width = Math.max(1, Math.round((img.naturalWidth || img.width) * scale));
const height = Math.max(1, Math.round((img.naturalHeight || img.height) * scale));
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
const blob = await canvasToBlob(canvas, quality);
if (blob.size <= MAX_UPLOAD_BYTES || maxDimension <= MIN_UPLOAD_DIMENSION) {
return new File([blob], `${stem}.jpg`, { type: "image/jpeg", lastModified: Date.now() });
}
maxDimension = Math.max(MIN_UPLOAD_DIMENSION, Math.round(maxDimension * 0.82));
quality = Math.max(0.62, quality - 0.05);
}
return file;
}
async function uploadAttachment() {
const file = attachInput.files[0];
if (!file) return;
saveMessage.textContent = "Uploading image.";
const body = new FormData();
body.append("attachment", file);
body.append("pagePath", currentEditorPath() || "index.org");
saveMessage.textContent = "Preparing image.";
try {
const uploadFile = await prepareImageUpload(file);
const body = new FormData();
body.append("attachment", uploadFile, uploadFile.name);
body.append("pagePath", currentEditorPath() || "index.org");
saveMessage.textContent = uploadFile.size < file.size ? "Uploading optimized image." : "Uploading image.";
const res = await fetch("/api/upload", { method: "POST", body });
const data = await res.json();
const data = await readApiResponse(res);
if (!res.ok) throw new Error(data.error || "Upload failed");
replaceSelection(`\n${data.insertText || data.markdown}\n`);
saveMessage.textContent = "Image inserted.";
} catch (err) {
saveMessage.textContent = err.message;
saveMessage.textContent = err.message || "Upload failed";
} finally {
attachInput.value = "";
}
@@ -3487,4 +3568,3 @@ HIDDEN_APP_HTML = r"""<!doctype html>
</body>
</html>
"""

View File

@@ -35,6 +35,13 @@ class Handler(BaseHTTPRequestHandler):
self.end_headers()
self.wfile.write(body)
def send_error(self, code: int, message: str | None = None, explain: str | None = None) -> None:
if urlparse(self.path).path.startswith("/api/"):
status = HTTPStatus(code)
self.send_json({"error": message or status.phrase}, status)
return
super().send_error(code, message, explain)
def do_GET(self) -> None:
parsed = urlparse(self.path)
if parsed.path == "/":