Launch
This commit is contained in:
@@ -1,217 +1,217 @@
|
||||
import { getConfiguredAssetBaseUrl } from './backendUrl';
|
||||
|
||||
const WIKIMEDIA_FILEPATH_SEGMENT = 'Special:FilePath/';
|
||||
const WIKIMEDIA_REDIRECT_BASE = 'https://commons.wikimedia.org/wiki/Special:FilePath/';
|
||||
|
||||
export const DEFAULT_PLANT_IMAGE_URI =
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg';
|
||||
|
||||
// Verified working fallback images per category (from main database URLs)
|
||||
const CATEGORY_FALLBACK_IMAGES: Record<string, string> = {
|
||||
succulent: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Aloe_Vera_houseplant.jpg/330px-Aloe_Vera_houseplant.jpg',
|
||||
flowering: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/AnthuriumAndraenum.jpg/330px-AnthuriumAndraenum.jpg',
|
||||
medicinal: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Lavandula_angustifolia_-_K%C3%B6hler%E2%80%93s_Medizinal-Pflanzen-087.jpg/330px-Lavandula_angustifolia_-_K%C3%B6hler%E2%80%93s_Medizinal-Pflanzen-087.jpg',
|
||||
tree: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/25/Ficus_benjamina.jpg/330px-Ficus_benjamina.jpg',
|
||||
hanging: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Money_Plant_%28Epipremnum_aureum%29_4.jpg/330px-Money_Plant_%28Epipremnum_aureum%29_4.jpg',
|
||||
patterned: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Starr_080716-9470_Calathea_crotalifera.jpg/330px-Starr_080716-9470_Calathea_crotalifera.jpg',
|
||||
pet_friendly: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Chlorophytum_comosum_01.jpg/330px-Chlorophytum_comosum_01.jpg',
|
||||
high_humidity: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Spathiphyllum_cochlearispathum_RTBG.jpg/330px-Spathiphyllum_cochlearispathum_RTBG.jpg',
|
||||
air_purifier: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg/330px-Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg',
|
||||
sun: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Echeveria_elegans_-_1.jpg/330px-Echeveria_elegans_-_1.jpg',
|
||||
low_light: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Zamioculcas_zamiifolia_1.jpg/330px-Zamioculcas_zamiifolia_1.jpg',
|
||||
easy: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Pilea_peperomioides_Chinese_money_plant.jpg/330px-Pilea_peperomioides_Chinese_money_plant.jpg',
|
||||
large: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Strelitzia_nicolai_3.jpg/330px-Strelitzia_nicolai_3.jpg',
|
||||
};
|
||||
|
||||
const CATEGORY_PRIORITY = [
|
||||
'succulent', 'flowering', 'medicinal', 'tree', 'hanging',
|
||||
'patterned', 'pet_friendly', 'high_humidity', 'air_purifier',
|
||||
'sun', 'low_light', 'easy', 'large',
|
||||
];
|
||||
|
||||
export const getCategoryFallbackImage = (categories: string[]): string => {
|
||||
for (const cat of CATEGORY_PRIORITY) {
|
||||
if (categories.includes(cat) && CATEGORY_FALLBACK_IMAGES[cat]) {
|
||||
return CATEGORY_FALLBACK_IMAGES[cat];
|
||||
}
|
||||
}
|
||||
return DEFAULT_PLANT_IMAGE_URI;
|
||||
};
|
||||
|
||||
const tryDecode = (value: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const decodeRepeatedly = (value: string, rounds = 3): string => {
|
||||
let current = value;
|
||||
for (let index = 0; index < rounds; index += 1) {
|
||||
const decoded = tryDecode(current);
|
||||
if (decoded === current) break;
|
||||
current = decoded;
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
type PlantManifestItem = {
|
||||
localImageUri?: string;
|
||||
sourceUri?: string;
|
||||
};
|
||||
|
||||
let manifestSourceByLocalUriCache: Map<string, string> | null = null;
|
||||
let wikimediaSearchCache: Record<string, string | null> | null = null;
|
||||
|
||||
const normalizePlantAssetPath = (value?: string | null): string | null => {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const withoutQuery = trimmed.split(/[?#]/)[0].replace(/\\/g, '/');
|
||||
const normalizedPath = withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`;
|
||||
if (!normalizedPath.startsWith('/plants/')) return null;
|
||||
if (normalizedPath.includes('..')) return null;
|
||||
return normalizedPath;
|
||||
};
|
||||
|
||||
const loadManifestSourceByLocalUri = (): Map<string, string> => {
|
||||
if (manifestSourceByLocalUriCache) return manifestSourceByLocalUriCache;
|
||||
|
||||
const nextCache = new Map<string, string>();
|
||||
try {
|
||||
const manifest = require('../server/public/plants/manifest.json') as { items?: PlantManifestItem[] };
|
||||
const items = Array.isArray(manifest?.items) ? manifest.items : [];
|
||||
for (const item of items) {
|
||||
const localImageUri = normalizePlantAssetPath(item?.localImageUri);
|
||||
const sourceUri = String(item?.sourceUri || '').trim();
|
||||
if (!localImageUri || !sourceUri) continue;
|
||||
nextCache.set(localImageUri, sourceUri);
|
||||
}
|
||||
} catch {
|
||||
// Keep empty cache when manifest is unavailable.
|
||||
}
|
||||
|
||||
manifestSourceByLocalUriCache = nextCache;
|
||||
return nextCache;
|
||||
};
|
||||
|
||||
const loadWikimediaSearchCache = (): Record<string, string | null> => {
|
||||
if (wikimediaSearchCache) return wikimediaSearchCache;
|
||||
|
||||
try {
|
||||
wikimediaSearchCache = require('../server/public/plants/wikimedia-search-cache.json') as Record<string, string | null>;
|
||||
} catch {
|
||||
wikimediaSearchCache = {};
|
||||
}
|
||||
|
||||
return wikimediaSearchCache;
|
||||
};
|
||||
|
||||
const resolveServerAssetPath = (rawPath: string): string | null => {
|
||||
const normalizedPath = normalizePlantAssetPath(rawPath);
|
||||
if (!normalizedPath) return null;
|
||||
|
||||
return `${getConfiguredAssetBaseUrl()}${normalizedPath}`;
|
||||
};
|
||||
|
||||
const unwrapMarkdownLink = (value: string): string => {
|
||||
const markdownLink = value.match(/^\[[^\]]+]\((https?:\/\/[^)]+)\)(.*)$/i);
|
||||
if (!markdownLink) return value;
|
||||
const [, url, suffix] = markdownLink;
|
||||
return `${url}${suffix || ''}`;
|
||||
};
|
||||
|
||||
const convertWikimediaFilePathUrl = (value: string): string | null => {
|
||||
const segmentIndex = value.indexOf(WIKIMEDIA_FILEPATH_SEGMENT);
|
||||
if (segmentIndex < 0) return null;
|
||||
|
||||
const fileNameStart = segmentIndex + WIKIMEDIA_FILEPATH_SEGMENT.length;
|
||||
const rawFileName = value.slice(fileNameStart).split(/[?#]/)[0].trim();
|
||||
if (!rawFileName) return null;
|
||||
|
||||
const decodedFileName = tryDecode(rawFileName).replace(/\s+/g, ' ').trim();
|
||||
const encodedFileName = encodeURIComponent(decodedFileName).replace(/%2F/g, '/');
|
||||
return `${WIKIMEDIA_REDIRECT_BASE}${encodedFileName}`;
|
||||
};
|
||||
|
||||
export const getWikimediaFilePathFromThumbnailUrl = (url: string): string | null => {
|
||||
if (!url.includes('upload.wikimedia.org/wikipedia/commons/thumb/')) return null;
|
||||
|
||||
// Example: https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg
|
||||
// The filename is the segment between the second-to-last and last slash (in many cases)
|
||||
// or use a more robust regex
|
||||
const parts = url.split('/');
|
||||
// Filter out empty parts from trailing slashes
|
||||
const filtered = parts.filter(p => !!p);
|
||||
if (filtered.length < 5) return null;
|
||||
|
||||
// For thumb URLs, the filename is typically the part before the last part (which is the px-filename)
|
||||
// but let's be careful.
|
||||
const lastName = filtered[filtered.length - 1];
|
||||
const secondLastName = filtered[filtered.length - 2];
|
||||
|
||||
// In thumb URLs, the last part is usually "330px-Filename.jpg"
|
||||
// The second to last is "Filename.jpg"
|
||||
if (lastName.includes('px-') && lastName.endsWith(secondLastName)) {
|
||||
try {
|
||||
return decodeURIComponent(secondLastName);
|
||||
} catch {
|
||||
return secondLastName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getPlantImageSourceFallbackUri = (rawUri?: string | null): string | null => {
|
||||
const localImageUri = normalizePlantAssetPath(rawUri);
|
||||
if (!localImageUri) return null;
|
||||
|
||||
const sourceUri = loadManifestSourceByLocalUri().get(localImageUri);
|
||||
if (!sourceUri) return null;
|
||||
|
||||
if (/^https?:\/\//i.test(sourceUri)) {
|
||||
return tryResolveImageUri(sourceUri);
|
||||
}
|
||||
|
||||
if (!sourceUri.startsWith('wikimedia-search:')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawQuery = sourceUri.slice('wikimedia-search:'.length).trim();
|
||||
const decodedQuery = decodeRepeatedly(rawQuery);
|
||||
if (!decodedQuery) return null;
|
||||
|
||||
const searchCache = loadWikimediaSearchCache();
|
||||
const cachedUrl = searchCache[decodedQuery]
|
||||
|| searchCache[rawQuery]
|
||||
|| searchCache[encodeURIComponent(decodedQuery)]
|
||||
|| null;
|
||||
|
||||
return cachedUrl ? tryResolveImageUri(cachedUrl) : null;
|
||||
};
|
||||
|
||||
export const tryResolveImageUri = (rawUri?: string | null): string | null => {
|
||||
if (!rawUri) return null;
|
||||
|
||||
const trimmed = rawUri.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const localAssetUri = resolveServerAssetPath(trimmed);
|
||||
if (localAssetUri) return localAssetUri;
|
||||
|
||||
const normalized = unwrapMarkdownLink(trimmed);
|
||||
const converted = convertWikimediaFilePathUrl(normalized);
|
||||
const candidate = (converted || normalized).replace(/^http:\/\//i, 'https://');
|
||||
|
||||
if (!/^(https?:\/\/|file:\/\/|content:\/\/|data:image\/|blob:)/i.test(candidate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
};
|
||||
|
||||
export const resolveImageUri = (rawUri?: string | null): string => {
|
||||
return tryResolveImageUri(rawUri) || DEFAULT_PLANT_IMAGE_URI;
|
||||
};
|
||||
import { getConfiguredAssetBaseUrl } from './backendUrl';
|
||||
|
||||
const WIKIMEDIA_FILEPATH_SEGMENT = 'Special:FilePath/';
|
||||
const WIKIMEDIA_REDIRECT_BASE = 'https://commons.wikimedia.org/wiki/Special:FilePath/';
|
||||
|
||||
export const DEFAULT_PLANT_IMAGE_URI =
|
||||
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg';
|
||||
|
||||
// Verified working fallback images per category (from main database URLs)
|
||||
const CATEGORY_FALLBACK_IMAGES: Record<string, string> = {
|
||||
succulent: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/05/Aloe_Vera_houseplant.jpg/330px-Aloe_Vera_houseplant.jpg',
|
||||
flowering: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/AnthuriumAndraenum.jpg/330px-AnthuriumAndraenum.jpg',
|
||||
medicinal: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Lavandula_angustifolia_-_K%C3%B6hler%E2%80%93s_Medizinal-Pflanzen-087.jpg/330px-Lavandula_angustifolia_-_K%C3%B6hler%E2%80%93s_Medizinal-Pflanzen-087.jpg',
|
||||
tree: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/25/Ficus_benjamina.jpg/330px-Ficus_benjamina.jpg',
|
||||
hanging: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Money_Plant_%28Epipremnum_aureum%29_4.jpg/330px-Money_Plant_%28Epipremnum_aureum%29_4.jpg',
|
||||
patterned: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/e7/Starr_080716-9470_Calathea_crotalifera.jpg/330px-Starr_080716-9470_Calathea_crotalifera.jpg',
|
||||
pet_friendly: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6c/Chlorophytum_comosum_01.jpg/330px-Chlorophytum_comosum_01.jpg',
|
||||
high_humidity: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Spathiphyllum_cochlearispathum_RTBG.jpg/330px-Spathiphyllum_cochlearispathum_RTBG.jpg',
|
||||
air_purifier: 'https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg/330px-Snake_Plant_%28Sansevieria_trifasciata_%27Laurentii%27%29.jpg',
|
||||
sun: 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/85/Echeveria_elegans_-_1.jpg/330px-Echeveria_elegans_-_1.jpg',
|
||||
low_light: 'https://upload.wikimedia.org/wikipedia/commons/thumb/c/cf/Zamioculcas_zamiifolia_1.jpg/330px-Zamioculcas_zamiifolia_1.jpg',
|
||||
easy: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Pilea_peperomioides_Chinese_money_plant.jpg/330px-Pilea_peperomioides_Chinese_money_plant.jpg',
|
||||
large: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/ba/Strelitzia_nicolai_3.jpg/330px-Strelitzia_nicolai_3.jpg',
|
||||
};
|
||||
|
||||
const CATEGORY_PRIORITY = [
|
||||
'succulent', 'flowering', 'medicinal', 'tree', 'hanging',
|
||||
'patterned', 'pet_friendly', 'high_humidity', 'air_purifier',
|
||||
'sun', 'low_light', 'easy', 'large',
|
||||
];
|
||||
|
||||
export const getCategoryFallbackImage = (categories: string[]): string => {
|
||||
for (const cat of CATEGORY_PRIORITY) {
|
||||
if (categories.includes(cat) && CATEGORY_FALLBACK_IMAGES[cat]) {
|
||||
return CATEGORY_FALLBACK_IMAGES[cat];
|
||||
}
|
||||
}
|
||||
return DEFAULT_PLANT_IMAGE_URI;
|
||||
};
|
||||
|
||||
const tryDecode = (value: string): string => {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const decodeRepeatedly = (value: string, rounds = 3): string => {
|
||||
let current = value;
|
||||
for (let index = 0; index < rounds; index += 1) {
|
||||
const decoded = tryDecode(current);
|
||||
if (decoded === current) break;
|
||||
current = decoded;
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
type PlantManifestItem = {
|
||||
localImageUri?: string;
|
||||
sourceUri?: string;
|
||||
};
|
||||
|
||||
let manifestSourceByLocalUriCache: Map<string, string> | null = null;
|
||||
let wikimediaSearchCache: Record<string, string | null> | null = null;
|
||||
|
||||
const normalizePlantAssetPath = (value?: string | null): string | null => {
|
||||
const trimmed = String(value || '').trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const withoutQuery = trimmed.split(/[?#]/)[0].replace(/\\/g, '/');
|
||||
const normalizedPath = withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`;
|
||||
if (!normalizedPath.startsWith('/plants/')) return null;
|
||||
if (normalizedPath.includes('..')) return null;
|
||||
return normalizedPath;
|
||||
};
|
||||
|
||||
const loadManifestSourceByLocalUri = (): Map<string, string> => {
|
||||
if (manifestSourceByLocalUriCache) return manifestSourceByLocalUriCache;
|
||||
|
||||
const nextCache = new Map<string, string>();
|
||||
try {
|
||||
const manifest = require('../server/public/plants/manifest.json') as { items?: PlantManifestItem[] };
|
||||
const items = Array.isArray(manifest?.items) ? manifest.items : [];
|
||||
for (const item of items) {
|
||||
const localImageUri = normalizePlantAssetPath(item?.localImageUri);
|
||||
const sourceUri = String(item?.sourceUri || '').trim();
|
||||
if (!localImageUri || !sourceUri) continue;
|
||||
nextCache.set(localImageUri, sourceUri);
|
||||
}
|
||||
} catch {
|
||||
// Keep empty cache when manifest is unavailable.
|
||||
}
|
||||
|
||||
manifestSourceByLocalUriCache = nextCache;
|
||||
return nextCache;
|
||||
};
|
||||
|
||||
const loadWikimediaSearchCache = (): Record<string, string | null> => {
|
||||
if (wikimediaSearchCache) return wikimediaSearchCache;
|
||||
|
||||
try {
|
||||
wikimediaSearchCache = require('../server/public/plants/wikimedia-search-cache.json') as Record<string, string | null>;
|
||||
} catch {
|
||||
wikimediaSearchCache = {};
|
||||
}
|
||||
|
||||
return wikimediaSearchCache;
|
||||
};
|
||||
|
||||
const resolveServerAssetPath = (rawPath: string): string | null => {
|
||||
const normalizedPath = normalizePlantAssetPath(rawPath);
|
||||
if (!normalizedPath) return null;
|
||||
|
||||
return `${getConfiguredAssetBaseUrl()}${normalizedPath}`;
|
||||
};
|
||||
|
||||
const unwrapMarkdownLink = (value: string): string => {
|
||||
const markdownLink = value.match(/^\[[^\]]+]\((https?:\/\/[^)]+)\)(.*)$/i);
|
||||
if (!markdownLink) return value;
|
||||
const [, url, suffix] = markdownLink;
|
||||
return `${url}${suffix || ''}`;
|
||||
};
|
||||
|
||||
const convertWikimediaFilePathUrl = (value: string): string | null => {
|
||||
const segmentIndex = value.indexOf(WIKIMEDIA_FILEPATH_SEGMENT);
|
||||
if (segmentIndex < 0) return null;
|
||||
|
||||
const fileNameStart = segmentIndex + WIKIMEDIA_FILEPATH_SEGMENT.length;
|
||||
const rawFileName = value.slice(fileNameStart).split(/[?#]/)[0].trim();
|
||||
if (!rawFileName) return null;
|
||||
|
||||
const decodedFileName = tryDecode(rawFileName).replace(/\s+/g, ' ').trim();
|
||||
const encodedFileName = encodeURIComponent(decodedFileName).replace(/%2F/g, '/');
|
||||
return `${WIKIMEDIA_REDIRECT_BASE}${encodedFileName}`;
|
||||
};
|
||||
|
||||
export const getWikimediaFilePathFromThumbnailUrl = (url: string): string | null => {
|
||||
if (!url.includes('upload.wikimedia.org/wikipedia/commons/thumb/')) return null;
|
||||
|
||||
// Example: https://upload.wikimedia.org/wikipedia/commons/thumb/2/2e/Monstera_deliciosa2.jpg/330px-Monstera_deliciosa2.jpg
|
||||
// The filename is the segment between the second-to-last and last slash (in many cases)
|
||||
// or use a more robust regex
|
||||
const parts = url.split('/');
|
||||
// Filter out empty parts from trailing slashes
|
||||
const filtered = parts.filter(p => !!p);
|
||||
if (filtered.length < 5) return null;
|
||||
|
||||
// For thumb URLs, the filename is typically the part before the last part (which is the px-filename)
|
||||
// but let's be careful.
|
||||
const lastName = filtered[filtered.length - 1];
|
||||
const secondLastName = filtered[filtered.length - 2];
|
||||
|
||||
// In thumb URLs, the last part is usually "330px-Filename.jpg"
|
||||
// The second to last is "Filename.jpg"
|
||||
if (lastName.includes('px-') && lastName.endsWith(secondLastName)) {
|
||||
try {
|
||||
return decodeURIComponent(secondLastName);
|
||||
} catch {
|
||||
return secondLastName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getPlantImageSourceFallbackUri = (rawUri?: string | null): string | null => {
|
||||
const localImageUri = normalizePlantAssetPath(rawUri);
|
||||
if (!localImageUri) return null;
|
||||
|
||||
const sourceUri = loadManifestSourceByLocalUri().get(localImageUri);
|
||||
if (!sourceUri) return null;
|
||||
|
||||
if (/^https?:\/\//i.test(sourceUri)) {
|
||||
return tryResolveImageUri(sourceUri);
|
||||
}
|
||||
|
||||
if (!sourceUri.startsWith('wikimedia-search:')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawQuery = sourceUri.slice('wikimedia-search:'.length).trim();
|
||||
const decodedQuery = decodeRepeatedly(rawQuery);
|
||||
if (!decodedQuery) return null;
|
||||
|
||||
const searchCache = loadWikimediaSearchCache();
|
||||
const cachedUrl = searchCache[decodedQuery]
|
||||
|| searchCache[rawQuery]
|
||||
|| searchCache[encodeURIComponent(decodedQuery)]
|
||||
|| null;
|
||||
|
||||
return cachedUrl ? tryResolveImageUri(cachedUrl) : null;
|
||||
};
|
||||
|
||||
export const tryResolveImageUri = (rawUri?: string | null): string | null => {
|
||||
if (!rawUri) return null;
|
||||
|
||||
const trimmed = rawUri.trim();
|
||||
if (!trimmed) return null;
|
||||
|
||||
const localAssetUri = resolveServerAssetPath(trimmed);
|
||||
if (localAssetUri) return localAssetUri;
|
||||
|
||||
const normalized = unwrapMarkdownLink(trimmed);
|
||||
const converted = convertWikimediaFilePathUrl(normalized);
|
||||
const candidate = (converted || normalized).replace(/^http:\/\//i, 'https://');
|
||||
|
||||
if (!/^(https?:\/\/|file:\/\/|content:\/\/|data:image\/|blob:)/i.test(candidate)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
};
|
||||
|
||||
export const resolveImageUri = (rawUri?: string | null): string => {
|
||||
return tryResolveImageUri(rawUri) || DEFAULT_PLANT_IMAGE_URI;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user