From d171e9705d961c272b8a001d2ead62a8ffe1d5ab Mon Sep 17 00:00:00 2001 From: Youness Fkhach Date: Sun, 7 Jun 2020 10:55:08 +0100 Subject: [PATCH] Fix RTL text direction rendering (reopened) (#1722) Co-authored-by: dwelle --- src/renderer/renderElement.ts | 35 ++++++++++++++++++++++++++++++----- src/utils.ts | 15 +++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index a965900d7..71b76855c 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -14,7 +14,13 @@ import { Drawable, Options } from "roughjs/bin/core"; import { RoughSVG } from "roughjs/bin/svg"; import { RoughGenerator } from "roughjs/bin/generator"; 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 rough from "roughjs/bin/rough"; @@ -100,12 +106,21 @@ const drawElementOnCanvas = ( } default: { 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; context.font = getFontString(element); const fillStyle = context.fillStyle; context.fillStyle = element.strokeColor; const textAlign = context.textAlign; context.textAlign = element.textAlign as CanvasTextAlign; + // Canvas does not support multiline text by default const lines = element.text.replace(/\r\n?/g, "\n").split("\n"); const lineHeight = element.height / lines.length; @@ -119,13 +134,16 @@ const drawElementOnCanvas = ( for (let i = 0; i < lines.length; i++) { context.fillText( lines[i], - 0 + horizontalOffset, + horizontalOffset, (i + 1) * lineHeight - verticalOffset, ); } context.fillStyle = fillStyle; context.font = font; context.textAlign = textAlign; + if (shouldTemporarilyAttach) { + context.canvas.remove(); + } } else { throw new Error(`Unimplemented type ${element.type}`); } @@ -342,6 +360,8 @@ const drawElementFromCanvas = ( context.rotate(-element.angle); context.translate(-cx, -cy); context.scale(window.devicePixelRatio, window.devicePixelRatio); + + // Clear the nested element we appended to the DOM }; export const renderElement = ( @@ -375,9 +395,12 @@ export const renderElement = ( case "draw": case "arrow": case "text": { - const elementWithCanvas = generateElement(element, generator, sceneState); - if (renderOptimizations) { + const elementWithCanvas = generateElement( + element, + generator, + sceneState, + ); drawElementFromCanvas(elementWithCanvas, rc, context, sceneState); } else { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); @@ -492,10 +515,11 @@ export const renderElementToSvg = ( : element.textAlign === "right" ? element.width : 0; + const direction = isRTL(element.text) ? "rtl" : "ltr"; const textAnchor = element.textAlign === "center" ? "middle" - : element.textAlign === "right" + : element.textAlign === "right" || direction === "rtl" ? "end" : "start"; for (let i = 0; i < lines.length; i++) { @@ -508,6 +532,7 @@ export const renderElementToSvg = ( text.setAttribute("fill", element.strokeColor); text.setAttribute("text-anchor", textAnchor); text.setAttribute("style", "white-space: pre;"); + text.setAttribute("direction", direction); node.appendChild(text); } svgRoot.appendChild(node); diff --git a/src/utils.ts b/src/utils.ts index b0224a59c..9c2f63894 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -227,3 +227,18 @@ export const sceneCoordsToViewportCoords = ( export const getGlobalCSSVariable = (name: string) => 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); +};