feat: Adjust line-height depending on script (#10565)

* migration

* Auto detect language and adjust line-height accordingly

* Remove accidental commit

* Remove unneccessary adjustment

* test

* mock
This commit is contained in:
Tom Moor
2025-11-08 11:47:51 -05:00
committed by GitHub
parent 265f2721f8
commit 3f3a70d996
13 changed files with 154 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
{
"workerIdleMemoryLimit": "0.75",
"maxWorkers": "50%",
"transformIgnorePatterns": ["node_modules/(?!(franc|trigram-utils)/)"],
"projects": [
{
"displayName": "server",

View File

@@ -144,6 +144,7 @@ export type Props = {
style?: React.CSSProperties;
/** Optional style overrides for the contenteeditable */
editorStyle?: React.CSSProperties;
lang?: string;
};
type State = {
@@ -846,7 +847,7 @@ export class Editor extends React.PureComponent<
editorStyle={this.props.editorStyle}
commenting={!!this.props.onClickCommentMark}
ref={this.elementRef}
lang=""
lang={this.props.lang ?? ""}
/>
{this.widgets &&

View File

@@ -134,6 +134,9 @@ export default class Document extends ArchivableModel implements Searchable {
@observable
title: string;
/** The likely language of the document. */
language: string | undefined;
/**
* An icon (or) emoji to use as the document icon.
*/

View File

@@ -229,6 +229,7 @@ function DocumentEditor(props: Props, ref: React.RefObject<any>) {
)}
<EditorComponent
ref={mergeRefs([ref, handleRefChanged])}
lang={document.language}
autoFocus={!!document.title && !props.defaultValue}
placeholder={t("Type '/' to insert, or start writing…")}
scrollTo={decodeURIComponentSafe(window.location.hash)}

View File

@@ -140,6 +140,7 @@
"form-data": "^4.0.4",
"fractional-index": "^1.0.0",
"framer-motion": "^4.1.17",
"franc": "^6.2.0",
"fs-extra": "^11.3.2",
"fuzzy-search": "^3.2.1",
"glob": "^8.1.0",
@@ -151,6 +152,7 @@
"invariant": "^2.2.4",
"ioredis": "^5.8.2",
"is-printable-key-event": "^1.0.0",
"iso-639-3": "^3.0.1",
"jsdom": "^22.1.0",
"jsonwebtoken": "^9.0.0",
"jszip": "^3.10.1",

10
server/__mocks__/franc.js Normal file
View File

@@ -0,0 +1,10 @@
// Mock for franc language detection library
const franc = jest.fn((text) => {
// Return 'eng' (English) by default, or 'und' (undetermined) for empty text
if (!text || text.trim().length === 0) {
return "und";
}
return "eng";
});
module.exports = { franc };

View File

@@ -0,0 +1,18 @@
// Mock for iso-639-3 language code conversion library
const iso6393To1 = {
eng: "en",
fra: "fr",
deu: "de",
spa: "es",
ita: "it",
por: "pt",
rus: "ru",
jpn: "ja",
zho: "zh",
ara: "ar",
hin: "hi",
ben: "bn",
und: undefined, // undetermined
};
module.exports = { iso6393To1 };

View File

@@ -0,0 +1,15 @@
"use strict";
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.addColumn("documents", "language", {
type: Sequelize.STRING(2),
allowNull: true,
});
},
async down(queryInterface, Sequelize) {
await queryInterface.removeColumn("documents", "language");
},
};

View File

@@ -70,6 +70,7 @@ import Fix from "./decorators/Fix";
import { DocumentHelper } from "./helpers/DocumentHelper";
import IsHexColor from "./validators/IsHexColor";
import Length from "./validators/Length";
import { MaxLength } from "class-validator";
export const DOCUMENT_VERSION = 2;
@@ -339,6 +340,11 @@ class Document extends ArchivableModel<
@Column(DataType.TEXT)
text: string;
/** The likely language of the content. */
@Column(DataType.STRING(2))
@MaxLength(2)
language: string;
/**
* The content of the document as JSON, this is a snapshot at the last time the state was saved.
*/

View File

@@ -57,6 +57,7 @@ async function presentDocument(
icon: document.icon,
color: document.color,
tasks: document.tasks,
language: document.language,
createdAt: document.createdAt,
createdBy: undefined,
updatedAt: document.updatedAt,

View File

@@ -1,7 +1,10 @@
import { franc } from "franc";
import { iso6393To1 } from "iso-639-3";
import { Node } from "prosemirror-model";
import { schema, serializer } from "@server/editor";
import { Document } from "@server/models";
import { DocumentEvent } from "@server/types";
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
import BaseTask from "./BaseTask";
export default class DocumentUpdateTextTask extends BaseTask<DocumentEvent> {
@@ -13,6 +16,10 @@ export default class DocumentUpdateTextTask extends BaseTask<DocumentEvent> {
const node = Node.fromJSON(schema, document.content);
document.text = serializer.serialize(node);
const language = franc(DocumentHelper.toPlainText(document));
document.language = iso6393To1[language];
await document.save({ silent: true });
}
}

View File

@@ -304,6 +304,63 @@ const emailStyle = (props: Props) => css`
}
`;
const textStyle = () => css`
/* Southeast Asian scripts */
:lang(th), /* Thai */
:lang(lo), /* Lao */
:lang(km), /* Khmer */
:lang(my) {
/* Burmese */
p {
line-height: 1.8;
}
}
/* South Asian scripts */
:lang(hi), /* Hindi */
:lang(mr), /* Marathi */
:lang(ne), /* Nepali */
:lang(bn), /* Bengali */
:lang(gu), /* Gujarati */
:lang(pa), /* Punjabi */
:lang(te), /* Telugu */
:lang(ta), /* Tamil */
:lang(ml), /* Malayalam */
:lang(si) {
/* Sinhala */
p {
line-height: 1.7;
}
}
/* Tibetan and related scripts */
:lang(bo) {
p {
line-height: 1.8;
}
}
/* Middle Eastern scripts */
:lang(ar), /* Arabic */
:lang(fa), /* Persian */
:lang(ur), /* Urdu */
:lang(he) {
/* Hebrew */
p {
line-height: 1.6;
}
}
/* Ethiopic and other complex scripts */
:lang(am), /* Amharic */
:lang(mn) {
/* Mongolian */
p {
line-height: 1.7;
}
}
`;
const style = (props: Props) => css`
flex-grow: ${props.grow ? 1 : 0};
justify-content: start;
@@ -2017,6 +2074,7 @@ const EditorContainer = styled.div<Props>`
${codeBlockStyle}
${findAndReplaceStyle}
${emailStyle}
${textStyle}
`;
export default EditorContainer;

View File

@@ -6688,6 +6688,11 @@ co@^4.6.0:
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ=="
collapse-white-space@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-2.1.0.tgz#640257174f9f42c740b40f3b55ee752924feefca"
integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==
collect-v8-coverage@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
@@ -8460,6 +8465,13 @@ framesync@5.3.0:
dependencies:
tslib "^2.1.0"
franc@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/franc/-/franc-6.2.0.tgz#2ced94b49b1df10bdebb8ab2cdfb57aa5551daa5"
integrity sha512-rcAewP7PSHvjq7Kgd7dhj82zE071kX5B4W1M4ewYMf/P+i6YsDQmj62Xz3VQm9zyUzUXwhIde/wHLGCMrM+yGg==
dependencies:
trigram-utils "^2.0.0"
fresh@~0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@@ -9647,6 +9659,11 @@ isexe@^3.1.1:
resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d"
integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==
iso-639-3@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/iso-639-3/-/iso-639-3-3.0.1.tgz#4be56987c46fbda79da63a3d90d6552d7429dcea"
integrity sha512-SdljCYXOexv/JmbQ0tvigHN43yECoscVpe2y2hlEqy/CStXQlroPhZLj7zKLRiGqLJfw8k7B973UAMDoQczVgQ==
isomorphic-unfetch@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz#87341d5f4f7b63843d468438128cb087b7c3e98f"
@@ -11374,6 +11391,11 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
n-gram@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/n-gram/-/n-gram-2.0.2.tgz#e544a7dffefc49c22d898b2f491e787941b3a2ba"
integrity sha512-S24aGsn+HLBxUGVAUFOwGpKs7LBcG4RudKU//eWzt/mQ97/NMKQxDWHyHx63UNWk/OOdihgmzoETn1tf5nQDzQ==
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
@@ -14286,6 +14308,14 @@ tree-kill@^1.2.2:
resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc"
integrity "sha1-TKCakJLIi3OnzcXooBtQeweQoMw= sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
trigram-utils@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/trigram-utils/-/trigram-utils-2.0.1.tgz#d22c08350f2cc7ae02ce6d497732db3ef43c8722"
integrity sha512-nfWIXHEaB+HdyslAfMxSqWKDdmqY9I32jS7GnqpdWQnLH89r6A5sdk3fDVYqGAZ0CrT8ovAFSAo6HRiWcWNIGQ==
dependencies:
collapse-white-space "^2.0.0"
n-gram "^2.0.0"
triple-beam@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"