/* * Copyright (C) 2026 Mateusz GruszczyƄski @linuxiarz.pl * Source-Available Code / Dual-Licensed. * * Free for non-commercial and evaluation use under terms of BSL/GPLv3. * Commercial or production use requires a valid paid license. * See LICENSE file in repository root for details. */ const IMAGE_ALIAS_SOURCE = String.raw`\[(image|img)=([^,\]\s]+)(?:,([^\]]*))?\]`; const ALIGNMENTS = new Set(["left", "center", "right"]); const MAX_IMAGE_DIMENSION = 10000; function cleanText(value, fallback = "") { return String(value ?? fallback) .replace(/\]/g, ")") .replace(/[\r\n]+/g, " ") .trim(); } function validDimension(value) { const number = Math.round(Number(value)); return Number.isFinite(number) && number >= 1 && number <= MAX_IMAGE_DIMENSION ? number : null; } function inlineCodeRanges(value) { const ranges = []; const pattern = /`[^`]+`/g; for (const match of String(value || "").matchAll(pattern)) { ranges.push([match.index, match.index + match[0].length]); } return ranges; } function isInsideRange(index, ranges) { return ranges.some(([start, end]) => index >= start && index < end); } export function imageAliasPattern(flags = "gi") { return new RegExp(IMAGE_ALIAS_SOURCE, flags); } export function parseImageAlias(value) { const match = String(value || "").match(new RegExp(`^${IMAGE_ALIAS_SOURCE}$`, "i")); if (!match) return null; const [, kind, filename, tail = ""] = match; const parts = tail.split(","); let align = null; let width = null; let height = null; while (parts.length) { const part = parts.at(-1).trim(); const alignment = part.match(/^a=(left|center|right)$/i); if (alignment) { align = alignment[1].toLowerCase(); parts.pop(); continue; } const size = part.match(/^size=(\d{1,5})x(\d{1,5})$/i); if (size) { const nextWidth = validDimension(size[1]); const nextHeight = validDimension(size[2]); if (nextWidth && nextHeight) { width = nextWidth; height = nextHeight; } parts.pop(); continue; } break; } return { kind: kind.toLowerCase(), filename, label: cleanText(parts.join(","), filename) || filename, align, width, height, raw: match[0], }; } export function buildImageAlias(options = {}) { const filename = cleanText(options.filename, "image").replace(/[,\s]/g, "_") || "image"; const label = cleanText(options.label, filename) || filename; const kind = String(options.kind || "image").toLowerCase() === "img" ? "img" : "image"; const align = ALIGNMENTS.has(String(options.align || "").toLowerCase()) ? String(options.align).toLowerCase() : null; const width = validDimension(options.width); const height = validDimension(options.height); const parts = [label]; if (align) parts.push(`a=${align}`); if (width && height) parts.push(`size=${width}x${height}`); return `[${kind}=${filename},${parts.join(",")}]`; } export function updateImageAliasInLine(line, aliasIndex, patch = {}) { const source = String(line || ""); const targetIndex = Number(aliasIndex); if (!Number.isInteger(targetIndex) || targetIndex < 0) return null; const codeRanges = inlineCodeRanges(source); let index = 0; let changed = false; const value = source.replace(imageAliasPattern(), (match, ...args) => { const offset = args.at(-2); if (isInsideRange(offset, codeRanges) || index++ !== targetIndex) return match; const parsed = parseImageAlias(match); if (!parsed) return match; changed = true; return buildImageAlias({ ...parsed, ...patch }); }); return changed ? value : null; } export function updateImageAliasInLineBySource(line, aliasSource, occurrence = 0, patch = {}) { const source = String(line || ""); const target = String(aliasSource || ""); const targetOccurrence = Number(occurrence); if (!target || !Number.isInteger(targetOccurrence) || targetOccurrence < 0) return null; const codeRanges = inlineCodeRanges(source); let matchedOccurrence = 0; let changed = false; const value = source.replace(imageAliasPattern(), (match, ...args) => { const offset = args.at(-2); if (isInsideRange(offset, codeRanges) || match !== target || matchedOccurrence++ !== targetOccurrence) return match; const parsed = parseImageAlias(match); if (!parsed) return match; changed = true; return buildImageAlias({ ...parsed, ...patch }); }); return changed ? value : null; }