paste files, images
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user