Fix RTL text direction rendering (reopened) (#1722)

Co-authored-by: dwelle <luzar.david@gmail.com>
pull/1732/head
Youness Fkhach 5 years ago committed by GitHub
parent f7c4efbd35
commit d171e9705d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,7 +14,13 @@ import { Drawable, Options } from "roughjs/bin/core";
import { RoughSVG } from "roughjs/bin/svg"; import { RoughSVG } from "roughjs/bin/svg";
import { RoughGenerator } from "roughjs/bin/generator"; import { RoughGenerator } from "roughjs/bin/generator";
import { SceneState } from "../scene/types"; import { SceneState } from "../scene/types";
import { SVG_NS, distance, getFontString, getFontFamilyString } from "../utils"; import {
SVG_NS,
distance,
getFontString,
getFontFamilyString,
isRTL,
} from "../utils";
import { isPathALoop } from "../math"; import { isPathALoop } from "../math";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
@ -100,12 +106,21 @@ const drawElementOnCanvas = (
} }
default: { default: {
if (isTextElement(element)) { if (isTextElement(element)) {
const rtl = isRTL(element.text);
const shouldTemporarilyAttach = rtl && !context.canvas.isConnected;
if (shouldTemporarilyAttach) {
// to correctly render RTL text mixed with LTR, we have to append it
// to the DOM
document.body.appendChild(context.canvas);
}
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
const font = context.font; const font = context.font;
context.font = getFontString(element); context.font = getFontString(element);
const fillStyle = context.fillStyle; const fillStyle = context.fillStyle;
context.fillStyle = element.strokeColor; context.fillStyle = element.strokeColor;
const textAlign = context.textAlign; const textAlign = context.textAlign;
context.textAlign = element.textAlign as CanvasTextAlign; context.textAlign = element.textAlign as CanvasTextAlign;
// Canvas does not support multiline text by default // Canvas does not support multiline text by default
const lines = element.text.replace(/\r\n?/g, "\n").split("\n"); const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = element.height / lines.length; const lineHeight = element.height / lines.length;
@ -119,13 +134,16 @@ const drawElementOnCanvas = (
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
context.fillText( context.fillText(
lines[i], lines[i],
0 + horizontalOffset, horizontalOffset,
(i + 1) * lineHeight - verticalOffset, (i + 1) * lineHeight - verticalOffset,
); );
} }
context.fillStyle = fillStyle; context.fillStyle = fillStyle;
context.font = font; context.font = font;
context.textAlign = textAlign; context.textAlign = textAlign;
if (shouldTemporarilyAttach) {
context.canvas.remove();
}
} else { } else {
throw new Error(`Unimplemented type ${element.type}`); throw new Error(`Unimplemented type ${element.type}`);
} }
@ -342,6 +360,8 @@ const drawElementFromCanvas = (
context.rotate(-element.angle); context.rotate(-element.angle);
context.translate(-cx, -cy); context.translate(-cx, -cy);
context.scale(window.devicePixelRatio, window.devicePixelRatio); context.scale(window.devicePixelRatio, window.devicePixelRatio);
// Clear the nested element we appended to the DOM
}; };
export const renderElement = ( export const renderElement = (
@ -375,9 +395,12 @@ export const renderElement = (
case "draw": case "draw":
case "arrow": case "arrow":
case "text": { case "text": {
const elementWithCanvas = generateElement(element, generator, sceneState);
if (renderOptimizations) { if (renderOptimizations) {
const elementWithCanvas = generateElement(
element,
generator,
sceneState,
);
drawElementFromCanvas(elementWithCanvas, rc, context, sceneState); drawElementFromCanvas(elementWithCanvas, rc, context, sceneState);
} else { } else {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
@ -492,10 +515,11 @@ export const renderElementToSvg = (
: element.textAlign === "right" : element.textAlign === "right"
? element.width ? element.width
: 0; : 0;
const direction = isRTL(element.text) ? "rtl" : "ltr";
const textAnchor = const textAnchor =
element.textAlign === "center" element.textAlign === "center"
? "middle" ? "middle"
: element.textAlign === "right" : element.textAlign === "right" || direction === "rtl"
? "end" ? "end"
: "start"; : "start";
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@ -508,6 +532,7 @@ export const renderElementToSvg = (
text.setAttribute("fill", element.strokeColor); text.setAttribute("fill", element.strokeColor);
text.setAttribute("text-anchor", textAnchor); text.setAttribute("text-anchor", textAnchor);
text.setAttribute("style", "white-space: pre;"); text.setAttribute("style", "white-space: pre;");
text.setAttribute("direction", direction);
node.appendChild(text); node.appendChild(text);
} }
svgRoot.appendChild(node); svgRoot.appendChild(node);

@ -227,3 +227,18 @@ export const sceneCoordsToViewportCoords = (
export const getGlobalCSSVariable = (name: string) => export const getGlobalCSSVariable = (name: string) =>
getComputedStyle(document.documentElement).getPropertyValue(`--${name}`); getComputedStyle(document.documentElement).getPropertyValue(`--${name}`);
const RS_LTR_CHARS =
"A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF" +
"\u2C00-\uFB1C\uFDFE-\uFE6F\uFEFD-\uFFFF";
const RS_RTL_CHARS = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC";
const RE_RTL_CHECK = new RegExp(`^[^${RS_LTR_CHARS}]*[${RS_RTL_CHARS}]`);
/**
* Checks whether first directional character is RTL. Meaning whether it starts
* with RTL characters, or indeterminate (numbers etc.) characters followed by
* RTL.
* See https://github.com/excalidraw/excalidraw/pull/1722#discussion_r436340171
*/
export const isRTL = (text: string) => {
return RE_RTL_CHECK.test(text);
};

Loading…
Cancel
Save