mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 08:06:13 +00:00
fix: render right-to-left mail right-to-left
The HTML and plain-text body documents had no direction, so an Arabic or Hebrew message was laid out left-to-right: left-aligned, with its sentence-final punctuation on the wrong side. Port the webmail's `dir="auto"` on the body of both documents so the first strong character picks the direction. An email that declares its own `dir` on its `<html>` or `<body>` tag keeps it; the parser merges those attributes into ours only while ours has none. The shrink-to-fit reporter widens its wrapper to the content's natural width and scales it from the top left. In a right-to-left body that over-wide block overflowed to the left instead, so a 600px newsletter scaled into the space left of the screen. A negative right margin now keeps the widened wrapper flush left in either direction, so the existing origin, pan clamp and pinch anchoring still hold.
This commit is contained in:
@@ -248,10 +248,15 @@ const HEIGHT_REPORTER = `
|
||||
if (Z.tx < minTx) Z.tx = minTx;
|
||||
if (Z.tx > 0) Z.tx = 0;
|
||||
w.style.width = Z.layoutW + 'px';
|
||||
// An over-wide block in a right-to-left body overflows to the left,
|
||||
// where the top-left origin and the pan clamp don't expect it. The
|
||||
// negative right margin keeps it flush left in either direction.
|
||||
w.style.marginRight = (Z.avail - Z.layoutW) + 'px';
|
||||
w.style.transform = 'translateX(' + Z.tx + 'px) scale(' + s + ')';
|
||||
} else {
|
||||
Z.tx = 0;
|
||||
w.style.width = '100%';
|
||||
w.style.marginRight = '';
|
||||
w.style.transform = 'none';
|
||||
}
|
||||
}
|
||||
@@ -266,6 +271,7 @@ const HEIGHT_REPORTER = `
|
||||
// Reset to full width so wrapping content reports its true overflow extent
|
||||
// (the widest unbreakable element) rather than a previously-scaled box.
|
||||
w.style.width = '100%';
|
||||
w.style.marginRight = '';
|
||||
var content = w.scrollWidth;
|
||||
Z.avail = avail;
|
||||
Z.layoutW = content > avail + 1 ? content : avail;
|
||||
|
||||
@@ -205,6 +205,20 @@ describe('prepareEmailHtml', () => {
|
||||
expect(html).toContain('max-height:134px; max-width:200px; width:100%;');
|
||||
expect(html).toContain('style="max-width:300px; width:100%"');
|
||||
});
|
||||
|
||||
it('lets the content pick its text direction', () => {
|
||||
expect(prepareEmailHtml('<p>مرحبا بالعالم.</p>').html).toContain('<body dir="auto">');
|
||||
});
|
||||
|
||||
it('keeps a direction the email declares on its own html or body tag', () => {
|
||||
const body = prepareEmailHtml('<body dir="rtl"><p>Newsletter مرحبا</p></body>').html;
|
||||
expect(body).not.toContain('dir="auto"');
|
||||
expect(body).toContain('<body dir="rtl">');
|
||||
const root = prepareEmailHtml('<html lang="ar" dir="rtl"><body><p>x</p></body></html>').html;
|
||||
expect(root).not.toContain('dir="auto"');
|
||||
// A dir on an inner element is not a document direction.
|
||||
expect(prepareEmailHtml('<p dir="rtl">x</p>').html).toContain('<body dir="auto">');
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapPlainTextEmail', () => {
|
||||
@@ -213,6 +227,10 @@ describe('wrapPlainTextEmail', () => {
|
||||
expect(wrapPlainTextEmail('x', { font: 'mono' })).toContain('ui-monospace');
|
||||
expect(wrapPlainTextEmail('x')).not.toContain('ui-monospace');
|
||||
});
|
||||
|
||||
it('lets the content pick its text direction', () => {
|
||||
expect(wrapPlainTextEmail('مرحبا بالعالم.')).toContain('<body dir="auto">');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasNativeDarkMode', () => {
|
||||
|
||||
+12
-2
@@ -526,6 +526,13 @@ function isWordHtml(html: string): boolean {
|
||||
return /class=["']?(?:Mso|WordSection)|<o:p[\s>/]|urn:schemas-microsoft-com:office:office/i.test(html);
|
||||
}
|
||||
|
||||
// The parser merges the attributes of an email's own <html>/<body> tags into
|
||||
// our document's where ours lack them, so a direction the sender declared
|
||||
// there applies as long as we don't set one first.
|
||||
function declaresOwnDirection(html: string): boolean {
|
||||
return /<(?:html|body)\b[^>]*\sdir\s*=/i.test(html);
|
||||
}
|
||||
|
||||
// "auto" spacing: only drop our gutter when the mail paints a full-bleed
|
||||
// background canvas (a width:100% element carrying a background colour) - the
|
||||
// one case where the gutter shows as a frame around the email's own
|
||||
@@ -633,6 +640,9 @@ export function prepareEmailHtml(innerHtml: string, options: WrapOptions = {}):
|
||||
const autoDropsGutter = hasStyleTag && !word && hasFullBleedCanvas(processed);
|
||||
const dropGutter = messageSpacing === 'edge' || (messageSpacing === 'auto' && autoDropsGutter);
|
||||
const bodyPadding = dropGutter ? '0' : '16px';
|
||||
// dir="auto" lets the first strong character pick the direction, so
|
||||
// right-to-left mail reads right-to-left.
|
||||
const bodyDir = declaresOwnDirection(processed) ? '' : ' dir="auto"';
|
||||
|
||||
const html = `<!doctype html>
|
||||
<html style="color-scheme: ${colorScheme};">
|
||||
@@ -643,7 +653,7 @@ export function prepareEmailHtml(innerHtml: string, options: WrapOptions = {}):
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<style>${baseStyles(bodyPadding)}${word ? WORD_HTML_CSS : ''}${darkCss}</style>
|
||||
</head>
|
||||
<body>${processed}<style>html,body{height:auto!important;min-height:0!important;max-height:none!important}</style></body>
|
||||
<body${bodyDir}>${processed}<style>html,body{height:auto!important;min-height:0!important;max-height:none!important}</style></body>
|
||||
</html>`;
|
||||
|
||||
return { html, applyInversion, hasNativeDark, blockedExternal };
|
||||
@@ -701,7 +711,7 @@ details > summary::-webkit-details-marker { display: none; }
|
||||
<meta http-equiv="Content-Security-Policy" content="${buildCsp(true)}">
|
||||
<style>${styles}</style>
|
||||
</head>
|
||||
<body>${cleaned}</body>
|
||||
<body dir="auto">${cleaned}</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user