FIX: Respect text direction inside quotes (#16004)

Meta topic: https://meta.discourse.org/t/rtl-direction-is-broken-in-quotes/217639?u=osama.

Posts in Discourse are by default always rendered in the same direction as the rest of site, for example if the site is RTL, a post in that site is always rendered RTL even if it's made of an LTR language entirely. However, this behavior can be changed by enabling the `support mixed text direction` site setting which makes our posts rendering engine consider each "paragraph" in the post and apply an appropriate direction (using the `dir` attribute) on it based on its content/language.

I put paragraph in quotes because technically we only loop through the immediate children of the HTML element that contains the post cooked HTML and do this direction check on them. Most of the time the immediate children are actually paragraphs, but not always. The direction of an element is determined by checking its `textContent` property against a regular expression that checks all characters are RTL characters and based on the regular expression result the `dir` attribute is set on the element.

This technique doesn't work so well on quotes because they may contain multiple paragraphs which may be in different languages/directions. For example: if a site's language is Arabic (RTL language) and the `support mixed text direction` setting is enabled, regular paragraphs outside quotes are rendered as expected with the right direction depending on the paragraph's language. However, paragraphs within a quote are all (incorrectly) rendered in a single direction, LTR or RTL, regardless of whether they're of different languages/directions or not.

The reason for this is that when we're determining the direction for the quote, it's considered as one element and the direction is set on the whole quote. But for complex quotes that contain mixed paragraphs, we need to be more surgical and apply direction on individual paragraphs/elements within the quote.

This commit adds special handling for quotes to ensure that:

* the quote top bar (the avatar plus the chevron and arrow) always match the site direction
* each immediate paragraph (`<p>` elements) under `<blockquote>` in the quote gets a direction based on its content.

For before/after screenshots, see PR #16004.
This commit is contained in:
Osama Sayegh 2022-02-23 10:26:45 +03:00 committed by GitHub
parent 599a72768c
commit 799e27d15d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 116 additions and 2 deletions

View File

@ -16,10 +16,14 @@ export function isLTR(text) {
export function setTextDirections(elem) {
for (let e of elem.children) {
if (e.textContent) {
if (e.tagName === "ASIDE" && e.classList.contains("quote")) {
setTextDirectionsForQuote(e);
} else {
e.setAttribute("dir", isRTL(e.textContent) ? "rtl" : "ltr");
}
}
}
}
export function siteDir() {
if (!_siteDir) {
@ -33,3 +37,15 @@ export function siteDir() {
export function isDocumentRTL() {
return siteDir() === "rtl";
}
function setTextDirectionsForQuote(quoteElem) {
for (let titleElem of quoteElem.querySelectorAll(".title")) {
titleElem.setAttribute("dir", siteDir());
}
for (let quoteParagraphElem of quoteElem.querySelectorAll("blockquote > p")) {
quoteParagraphElem.setAttribute(
"dir",
isRTL(quoteParagraphElem.textContent) ? "rtl" : "ltr"
);
}
}

View File

@ -1,6 +1,35 @@
import { isLTR, isRTL } from "discourse/lib/text-direction";
import { isLTR, isRTL, setTextDirections } from "discourse/lib/text-direction";
import { module, test } from "qunit";
function quoteHtml() {
return `
<aside class="quote">
<div class="title">
<img src="avatar.png"> osama:
</div>
<blockquote>
<p>كلمات بالعربي هنا</p>
<p>English words here!!</p>
<aside class="quote">
<div class="title">
<img src="avatar.png"> أسامة:
</div>
<blockquote>
<p>English words here (nested quote)</p>
<p>كلمات بالعربي هنا (اقتباس متداخل)</p>
</blockquote>
</aside>
</blockquote>
</aside>
<p>Testing quotes with mixed text direction!</p>
<p>تجربة الاقتباسات مع نصوص باتجاهات مختلفة</p>
`;
}
function assertDirection(assert, elem, expected, message) {
assert.strictEqual(elem.getAttribute("dir"), expected, message);
}
module("Unit | Utility | text-direction", function () {
test("isRTL", function (assert) {
// Hebrew
@ -20,4 +49,73 @@ module("Unit | Utility | text-direction", function () {
assert.strictEqual(isLTR("This is a test"), true);
assert.strictEqual(isLTR("זה מבחן"), false);
});
test("setTextDirections", function (assert) {
const cooked = document.createElement("div");
cooked.classList.add("cooked");
cooked.innerHTML = quoteHtml();
setTextDirections(cooked);
const [englishTitle, arabicTitle] = Array.from(
cooked.querySelectorAll(".title")
);
assertDirection(
assert,
englishTitle,
"ltr",
"quote title always matches site direction regardless of its content"
);
assertDirection(
assert,
arabicTitle,
"ltr",
"quote title always matches site direction regardless of its content"
);
const [
quotedRtl,
quotedLtr,
quotedLtrNested,
quotedRtlNested,
notQuotedLtr,
notQuotedRtl,
] = Array.from(cooked.querySelectorAll("p"));
assertDirection(
assert,
quotedRtl,
"rtl",
"RTL paragraphs inside quote have rtl dir"
);
assertDirection(
assert,
quotedLtr,
"ltr",
"LTR paragraphs inside quote have ltr dir"
);
assertDirection(
assert,
quotedLtrNested,
"ltr",
"LTR paragraphs inside nested quote have ltr dir"
);
assertDirection(
assert,
quotedRtlNested,
"rtl",
"RTL paragraphs inside nested quote have rtl dir"
);
assertDirection(
assert,
notQuotedLtr,
"ltr",
"LTR paragraphs outside quotes have ltr dir"
);
assertDirection(
assert,
notQuotedRtl,
"rtl",
"RTL paragraphs outside quotes have rtl dir"
);
});
});