Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/elements/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const urlPattern = /^([^\s]+)/;
const absoluteUrlPattern = /^https?:\/\//;
const filenamePattern = /^[\w\-\.\/\\]+\.(jpg|jpeg|png|gif|webp|svg)$/i;
const datePattern = /^\d{4}-\d{2}-\d{2}$/;
const lazyImageSourceAttributes = ['data-src', 'data-original', 'data-lazy-src', 'data-actualsrc', 'data-backup'];

export const imageRules = [
// Handle picture elements first to ensure we get the highest resolution
Expand Down Expand Up @@ -142,7 +143,7 @@ export const imageRules = [

// Handle lazy-loaded images
{
selector: 'img[data-src], img[data-srcset], img[loading="lazy"], img.lazy, img.lazyload, img[src^="data:image/svg+xml"]',
selector: 'img[data-src], img[data-original], img[data-lazy-src], img[data-actualsrc], img[data-backup], img[data-srcset], img[loading="lazy"], img.lazy, img.lazyload, img[src^="data:image/"]',
element: 'img',
transform: (el: Element, doc: Document): Element => {
// Check for base64 placeholder images
Expand All @@ -154,10 +155,13 @@ export const imageRules = [
el.removeAttribute('src');
}

// Handle data-src
const dataSrc = el.getAttribute('data-src');
if (dataSrc && !el.getAttribute('src')) {
el.setAttribute('src', dataSrc);
// Handle common lazy image source attributes
for (const attrName of lazyImageSourceAttributes) {
const lazySrc = el.getAttribute(attrName);
if (lazySrc && !el.getAttribute('src')) {
el.setAttribute('src', lazySrc);
break;
}
}

// Handle data-srcset
Expand Down Expand Up @@ -197,7 +201,7 @@ export const imageRules = [
// Remove lazy loading related classes and attributes
el.classList.remove('lazy', 'lazyload');
el.removeAttribute('data-ll-status');
el.removeAttribute('data-src');
lazyImageSourceAttributes.forEach(attr => el.removeAttribute(attr));
el.removeAttribute('data-srcset');
el.removeAttribute('loading');

Expand Down Expand Up @@ -408,8 +412,8 @@ function isValidImageUrl(src: string): boolean {
* Check if an element has better image sources than the current src
*/
function hasBetterImageSource(element: Element): boolean {
// Check for data-src or data-srcset
if (element.hasAttribute('data-src') || element.hasAttribute('data-srcset')) {
// Check for common lazy image source attributes
if (element.hasAttribute('data-srcset') || lazyImageSourceAttributes.some(attr => element.hasAttribute(attr))) {
return true;
}

Expand Down
4 changes: 3 additions & 1 deletion src/removals/small-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ export function removeSmallImages(doc: Document, smallImages: Set<string>, debug
element.getAttribute('data-src') ||
element.getAttribute('data-srcset') ||
element.getAttribute('data-lazy-src') ||
element.getAttribute('data-original');
element.getAttribute('data-original') ||
element.getAttribute('data-actualsrc') ||
element.getAttribute('data-backup');
if (!src && !hasAltSrc) {
element.remove();
removedCount++;
Expand Down
16 changes: 16 additions & 0 deletions tests/markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,22 @@ describe('Markdown conversion', () => {
});
});

describe('lazy-loaded images', () => {
test.each([
'data-original',
'data-lazy-src',
'data-actualsrc',
'data-backup',
])('should promote %s when src is a placeholder', async (attribute) => {
const placeholder = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
const html = `<html><head><title>Test</title></head><body><article><p>Content</p><img src="${placeholder}" ${attribute}="/images/a.jpg" alt="A"></article></body></html>`;
const result = await Defuddle(parseDocument(html, 'https://example.com/articles/post'), 'https://example.com/articles/post', { separateMarkdown: true });

expect(result.contentMarkdown).toContain('![A](https://example.com/images/a.jpg)');
expect(result.contentMarkdown).not.toContain('data:image');
});
});

describe('wbr tag handling', () => {
test('should remove wbr tags without adding spaces', async () => {
const html = `<html><head><title>Test</title></head><body><article><p>Super<wbr>cali<wbr>fragilistic</p></article></body></html>`;
Expand Down