Merge pull request #2952 from standardnotes/chore/reapply-reverted-changes

chore: reapply pdf fonts fix and lexical upgrade
This commit is contained in:
Antonella Sgarlatta 2025-10-28 14:38:41 -03:00 committed by GitHub
commit 8f318f02a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
103 changed files with 788 additions and 352 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -108,17 +108,18 @@
},
"dependencies": {
"@ariakit/react": "^0.4.18",
"@lexical/clipboard": "0.32.1",
"@lexical/headless": "0.32.1",
"@lexical/link": "0.32.1",
"@lexical/list": "0.32.1",
"@lexical/react": "0.32.1",
"@lexical/rich-text": "0.32.1",
"@lexical/utils": "0.32.1",
"@lexical/clipboard": "0.38.1",
"@lexical/headless": "0.38.1",
"@lexical/link": "0.38.1",
"@lexical/list": "0.38.1",
"@lexical/react": "0.38.1",
"@lexical/rich-text": "0.38.1",
"@lexical/utils": "0.38.1",
"@radix-ui/react-slot": "^1.0.1",
"@react-pdf/renderer": "^3.3.2",
"@react-pdf/renderer": "^4.3.0",
"comlink": "^4.4.1",
"fast-diff": "^1.3.0",
"lexical": "0.32.1"
"lexical": "0.38.1",
"unicode-script": "^1.2.0"
}
}

View file

@ -0,0 +1,288 @@
import { Font } from '@react-pdf/renderer'
import { LexicalNode } from 'lexical'
// @ts-expect-error No typing for this package
import { unicodeScripts } from 'unicode-script'
enum UnicodeScript {
Latin = 'Latin',
Common = 'Common',
Cyrillic = 'Cyrillic',
Greek = 'Greek',
Hebrew = 'Hebrew',
Arabic = 'Arabic',
Devanagari = 'Devanagari',
Bengali = 'Bengali',
Tamil = 'Tamil',
Telugu = 'Telugu',
Gujarati = 'Gujarati',
Gurmukhi = 'Gurmukhi',
Malayalam = 'Malayalam',
Sinhala = 'Sinhala',
Thai = 'Thai',
Armenian = 'Armenian',
Georgian = 'Georgian',
Ethiopic = 'Ethiopic',
Myanmar = 'Myanmar',
Khmer = 'Khmer',
Lao = 'Lao',
Tibetan = 'Tibetan',
Vietnamese = 'Vietnamese',
Chinese = 'Chinese',
Han = 'Han',
Japanese = 'Japanese',
Korean = 'Korean',
Hangul = 'Hangul',
}
export enum FontFamily {
NotoSans = 'Noto Sans',
NotoSansHebrew = 'Noto Sans Hebrew',
NotoSansArabic = 'Noto Sans Arabic',
NotoSansDevanagari = 'Noto Sans Devanagari',
NotoSansBengali = 'Noto Sans Bengali',
NotoSansTamil = 'Noto Sans Tamil',
NotoSansTelugu = 'Noto Sans Telugu',
NotoSansGujarati = 'Noto Sans Gujarati',
NotoSansGurmukhi = 'Noto Sans Gurmukhi',
NotoSansMalayalam = 'Noto Sans Malayalam',
NotoSansSinhala = 'Noto Sans Sinhala',
NotoSansThai = 'Noto Sans Thai',
NotoSansArmenian = 'Noto Sans Armenian',
NotoSansGeorgian = 'Noto Sans Georgian',
NotoSansEthiopic = 'Noto Sans Ethiopic',
NotoSansMyanmar = 'Noto Sans Myanmar',
NotoSansKhmer = 'Noto Sans Khmer',
NotoSansLao = 'Noto Sans Lao',
NotoSansTibetan = 'Noto Sans Tibetan',
NotoSansSC = 'Noto Sans SC',
NotoSansJP = 'Noto Sans JP',
NotoSansKR = 'Noto Sans KR',
Courier = 'Courier',
Helvetica = 'Helvetica',
}
enum FontVariant {
Normal = 'normal',
Bold = 'bold',
Italic = 'italic',
BoldItalic = 'bolditalic',
}
type FontWeight = 'normal' | 'bold'
type FontStyle = 'normal' | 'italic'
const FONT_VARIANT_TO_FONT_OPTIONS: Record<FontVariant, { fontWeight: FontWeight; fontStyle: FontStyle }> = {
[FontVariant.Normal]: {
fontWeight: 'normal',
fontStyle: 'normal',
},
[FontVariant.Bold]: {
fontWeight: 'bold',
fontStyle: 'normal',
},
[FontVariant.Italic]: {
fontWeight: 'normal',
fontStyle: 'italic',
},
[FontVariant.BoldItalic]: {
fontWeight: 'bold',
fontStyle: 'italic',
},
}
const FONT_ASSETS_BASE_PATH =
process.env.NODE_ENV === 'development'
? 'http://localhost:3001/assets/fonts'
: 'https://assets.standardnotes.com/fonts'
const FALLBACK_FONT_SOURCE = '/noto-sans/NotoSans-Regular.ttf'
export const FALLBACK_FONT_FAMILY = FontFamily.Helvetica
export const MONOSPACE_FONT_FAMILY = FontFamily.Courier
const FONT_FAMILY_TO_FONT_SOURCES: Partial<Record<FontFamily, Partial<Record<FontVariant, string>>>> = {
[FontFamily.NotoSans]: {
[FontVariant.Normal]: '/noto-sans/NotoSans-Regular.ttf',
[FontVariant.Bold]: '/noto-sans/NotoSans-Bold.ttf',
[FontVariant.Italic]: '/noto-sans/NotoSans-Italic.ttf',
[FontVariant.BoldItalic]: '/noto-sans/NotoSans-BoldItalic.ttf',
},
[FontFamily.NotoSansHebrew]: {
[FontVariant.Normal]: '/noto-sans-hebrew/NotoSansHebrew-Regular.ttf',
[FontVariant.Bold]: '/noto-sans-hebrew/NotoSansHebrew-Bold.ttf',
},
[FontFamily.NotoSansArabic]: {
[FontVariant.Normal]: '/noto-sans-arabic/NotoSansArabic-Regular.ttf',
[FontVariant.Bold]: '/noto-sans-arabic/NotoSansArabic-Bold.ttf',
},
[FontFamily.NotoSansDevanagari]: {
[FontVariant.Normal]: '/noto-sans-devanagari/NotoSansDevanagari-Regular.ttf',
[FontVariant.Bold]: '/noto-sans-devanagari/NotoSansDevanagari-Bold.ttf',
},
[FontFamily.NotoSansBengali]: {
[FontVariant.Normal]: '/noto-sans-bengali/NotoSansBengali-Regular.ttf',
},
[FontFamily.NotoSansTamil]: {
[FontVariant.Normal]: '/noto-sans-tamil/NotoSansTamil-Regular.ttf',
},
[FontFamily.NotoSansTelugu]: {
[FontVariant.Normal]: '/noto-sans-telugu/NotoSansTelugu-Regular.ttf',
},
[FontFamily.NotoSansGujarati]: {
[FontVariant.Normal]: '/noto-sans-gujarati/NotoSansGujarati-Regular.ttf',
},
[FontFamily.NotoSansGurmukhi]: {
[FontVariant.Normal]: '/noto-sans-gurmukhi/NotoSansGurmukhi-Regular.ttf',
},
[FontFamily.NotoSansMalayalam]: {
[FontVariant.Normal]: '/noto-sans-malayalam/NotoSansMalayalam-Regular.ttf',
},
[FontFamily.NotoSansSinhala]: {
[FontVariant.Normal]: '/noto-sans-sinhala/NotoSansSinhala-Regular.ttf',
},
[FontFamily.NotoSansThai]: {
[FontVariant.Normal]: '/noto-sans-thai/NotoSansThai-Regular.ttf',
},
[FontFamily.NotoSansArmenian]: {
[FontVariant.Normal]: '/noto-sans-armenian/NotoSansArmenian-Regular.ttf',
},
[FontFamily.NotoSansGeorgian]: {
[FontVariant.Normal]: '/noto-sans-georgian/NotoSansGeorgian-Regular.ttf',
},
[FontFamily.NotoSansEthiopic]: {
[FontVariant.Normal]: '/noto-sans-ethiopic/NotoSansEthiopic-Regular.ttf',
},
[FontFamily.NotoSansMyanmar]: {
[FontVariant.Normal]: '/noto-sans-myanmar/NotoSansMyanmar-Regular.ttf',
},
[FontFamily.NotoSansKhmer]: {
[FontVariant.Normal]: '/noto-sans-khmer/NotoSansKhmer-Regular.ttf',
},
[FontFamily.NotoSansLao]: {
[FontVariant.Normal]: '/noto-sans-lao/NotoSansLao-Regular.ttf',
},
[FontFamily.NotoSansTibetan]: {
[FontVariant.Normal]: '/noto-sans-tibetan/NotoSansTibetan-Regular.ttf',
},
[FontFamily.NotoSansSC]: {
[FontVariant.Normal]: '/noto-sans-sc/NotoSansSC-Regular.ttf',
},
[FontFamily.NotoSansJP]: {
[FontVariant.Normal]: '/noto-sans-jp/NotoSansJP-Regular.ttf',
},
[FontFamily.NotoSansKR]: {
[FontVariant.Normal]: '/noto-sans-kr/NotoSansKR-Regular.ttf',
},
}
export const getFontFamilyForUnicodeScript = (script: UnicodeScript): FontFamily => {
switch (script) {
case UnicodeScript.Common:
case UnicodeScript.Latin:
case UnicodeScript.Cyrillic:
case UnicodeScript.Greek:
case UnicodeScript.Vietnamese:
return FontFamily.NotoSans
case UnicodeScript.Hebrew:
return FontFamily.NotoSansHebrew
case UnicodeScript.Arabic:
return FontFamily.NotoSansArabic
case UnicodeScript.Devanagari:
return FontFamily.NotoSansDevanagari
case UnicodeScript.Bengali:
return FontFamily.NotoSansBengali
case UnicodeScript.Tamil:
return FontFamily.NotoSansTamil
case UnicodeScript.Telugu:
return FontFamily.NotoSansTelugu
case UnicodeScript.Gujarati:
return FontFamily.NotoSansGujarati
case UnicodeScript.Gurmukhi:
return FontFamily.NotoSansGurmukhi
case UnicodeScript.Malayalam:
return FontFamily.NotoSansMalayalam
case UnicodeScript.Sinhala:
return FontFamily.NotoSansSinhala
case UnicodeScript.Thai:
return FontFamily.NotoSansThai
case UnicodeScript.Armenian:
return FontFamily.NotoSansArmenian
case UnicodeScript.Georgian:
return FontFamily.NotoSansGeorgian
case UnicodeScript.Ethiopic:
return FontFamily.NotoSansEthiopic
case UnicodeScript.Myanmar:
return FontFamily.NotoSansMyanmar
case UnicodeScript.Khmer:
return FontFamily.NotoSansKhmer
case UnicodeScript.Lao:
return FontFamily.NotoSansLao
case UnicodeScript.Tibetan:
return FontFamily.NotoSansTibetan
case UnicodeScript.Chinese:
case UnicodeScript.Han:
return FontFamily.NotoSansSC
case UnicodeScript.Japanese:
return FontFamily.NotoSansJP
case UnicodeScript.Korean:
case UnicodeScript.Hangul:
return FontFamily.NotoSansKR
default:
return FontFamily.NotoSans
}
}
const getFontRegisterOptions = (fontFamily: FontFamily) => {
const fallback = FONT_FAMILY_TO_FONT_SOURCES[fontFamily]?.[FontVariant.Normal] ?? FALLBACK_FONT_SOURCE
return {
family: fontFamily,
fonts: Object.entries(FONT_VARIANT_TO_FONT_OPTIONS).map(([variant, fontOptions]) => ({
...fontOptions,
src: `${FONT_ASSETS_BASE_PATH}${FONT_FAMILY_TO_FONT_SOURCES[fontFamily]?.[variant as FontVariant] ?? fallback}`,
})),
}
}
export const getFontFamiliesFromLexicalNode = (node: LexicalNode) => {
const scripts: UnicodeScript[] = Array.from(unicodeScripts(node.getTextContent()))
const fontFamilies = [FontFamily.NotoSans]
scripts.forEach((script) => {
const fontFamilyForScript = getFontFamilyForUnicodeScript(script)
if (!fontFamilies.includes(fontFamilyForScript)) {
fontFamilies.unshift(fontFamilyForScript)
}
})
const fontFamiliesSet = new Set(fontFamilies)
return Array.from(fontFamiliesSet)
}
export const registerPDFFonts = (fontFamilies: FontFamily[]) => {
const fontFamiliesToRegister = new Set(fontFamilies)
fontFamiliesToRegister.forEach((fontFamily) => {
const registerOptions = getFontRegisterOptions(fontFamily)
Font.register(registerOptions)
})
}

View file

@ -24,6 +24,7 @@ import { $isCollapsibleTitleNode } from '../../../Plugins/CollapsiblePlugin/Coll
import PDFWorker, { PDFDataNode, PDFWorkerInterface } from './PDFWorker.worker'
import { wrap } from 'comlink'
import { PrefKey, PrefValue } from '@standardnotes/snjs'
import { FALLBACK_FONT_FAMILY, FontFamily, MONOSPACE_FONT_FAMILY, getFontFamiliesFromLexicalNode } from './FontConfig'
const styles = StyleSheet.create({
page: {
@ -144,6 +145,12 @@ const getFontSizeForHeading = (heading: HeadingNode) => {
}
const getNodeTextAlignment = (node: ElementNode) => {
const direction = node.getDirection()
if (direction === 'rtl') {
return 'right'
}
const formatType = node.getFormatType()
if (!formatType) {
@ -161,7 +168,12 @@ const getNodeTextAlignment = (node: ElementNode) => {
return formatType
}
const getPDFDataNodeFromLexicalNode = (node: LexicalNode): PDFDataNode => {
const getNodeDirection = (node: ElementNode) => {
const direction = node.getDirection()
return direction ?? 'ltr'
}
const getPDFDataNodeFromLexicalNode = (node: LexicalNode, fontFamilies: FontFamily[]): PDFDataNode => {
const parent = node.getParent()
if ($isLineBreakNode(node)) {
@ -177,23 +189,23 @@ const getPDFDataNodeFromLexicalNode = (node: LexicalNode): PDFDataNode => {
const isBold = node.hasFormat('bold')
const isItalic = node.hasFormat('italic')
const isHighlight = node.hasFormat('highlight')
const nodeFontFamilies = getFontFamiliesFromLexicalNode(node)
let fontFamily: FontFamily[] | FontFamily = [...nodeFontFamilies, FALLBACK_FONT_FAMILY]
let font = isInlineCode || isCodeNodeText ? 'Courier' : 'Helvetica'
if (isBold || isItalic) {
font += '-'
if (isBold) {
font += 'Bold'
}
if (isItalic) {
font += 'Oblique'
}
if (isInlineCode && isCodeNodeText) {
fontFamily = MONOSPACE_FONT_FAMILY
} else {
fontFamilies.push(...nodeFontFamilies)
}
return {
type: 'Text',
children: node.getTextContent(),
style: {
fontFamily: font,
fontFamily,
fontWeight: isBold ? 'bold' : 'normal',
fontStyle: isItalic ? 'italic' : 'normal',
direction: $isElementNode(parent) ? getNodeDirection(parent) : 'ltr',
textDecoration: node.hasFormat('underline')
? 'underline'
: node.hasFormat('strikethrough')
@ -237,7 +249,7 @@ const getPDFDataNodeFromLexicalNode = (node: LexicalNode): PDFDataNode => {
type: 'View',
style: [styles.row, styles.wrap],
children: line.map((child) => {
return getPDFDataNodeFromLexicalNode(child)
return getPDFDataNodeFromLexicalNode(child, fontFamilies)
}),
}
}),
@ -267,7 +279,7 @@ const getPDFDataNodeFromLexicalNode = (node: LexicalNode): PDFDataNode => {
const children =
$isElementNode(node) || $isTableNode(node) || $isTableCellNode(node) || $isTableRowNode(node)
? node.getChildren().map((child) => {
return getPDFDataNodeFromLexicalNode(child)
return getPDFDataNodeFromLexicalNode(child, fontFamilies)
})
: undefined
@ -427,8 +439,8 @@ const getPDFDataNodeFromLexicalNode = (node: LexicalNode): PDFDataNode => {
}
}
const getPDFDataNodesFromLexicalNodes = (nodes: LexicalNode[]): PDFDataNode[] => {
return nodes.map(getPDFDataNodeFromLexicalNode)
const getPDFDataNodesFromLexicalNodes = (nodes: LexicalNode[], fontFamilies: FontFamily[]): PDFDataNode[] => {
return nodes.map((node) => getPDFDataNodeFromLexicalNode(node, fontFamilies))
}
const pdfWorker = new PDFWorker()
@ -438,17 +450,21 @@ const PDFWorkerComlink = wrap<PDFWorkerInterface>(pdfWorker)
* @returns The PDF as an object url
*/
export function $generatePDFFromNodes(editor: LexicalEditor, pageSize: PrefValue[PrefKey.SuperNoteExportPDFPageSize]) {
return new Promise<string>((resolve) => {
return new Promise<string>((resolve, reject) => {
editor.getEditorState().read(() => {
const root = $getRoot()
const nodes = root.getChildren()
const fontFamilies: FontFamily[] = []
const pdfDataNodes = getPDFDataNodesFromLexicalNodes(nodes, fontFamilies)
const pdfDataNodes = getPDFDataNodesFromLexicalNodes(nodes)
void PDFWorkerComlink.renderPDF(pdfDataNodes, pageSize).then((blob) => {
const url = URL.createObjectURL(blob)
resolve(url)
})
void PDFWorkerComlink.renderPDF(pdfDataNodes, pageSize, fontFamilies)
.then((blob) => {
const url = URL.createObjectURL(blob)
resolve(url)
})
.catch((error) => {
reject(error)
})
})
})
}

View file

@ -17,6 +17,7 @@ import {
PageProps,
} from '@react-pdf/renderer'
import { expose } from 'comlink'
import { FontFamily, registerPDFFonts } from './FontConfig'
export type PDFDataNode =
| ((
@ -94,7 +95,8 @@ const PDFDocument = ({ nodes, pageSize }: { nodes: PDFDataNode[]; pageSize: Page
)
}
const renderPDF = (nodes: PDFDataNode[], pageSize: PageProps['size']) => {
const renderPDF = (nodes: PDFDataNode[], pageSize: PageProps['size'], fontFamilies: FontFamily[]) => {
registerPDFFonts(fontFamilies)
return pdf(<PDFDocument pageSize={pageSize} nodes={nodes} />).toBlob()
}

View file

@ -64,6 +64,7 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
}
},
): Promise<string> {
let didThrow = false
if (superString.length === 0) {
return superString
}
@ -81,7 +82,7 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
let content: string | undefined
await new Promise<void>((resolve) => {
await new Promise<void>((resolve, reject) => {
const handleFileNodes = () => {
if (embedBehavior === 'reference') {
resolve()
@ -136,12 +137,16 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
}),
)
.then(() => resolve())
.catch(console.error)
.catch((error) => {
didThrow = true
console.error(error)
reject(error)
})
}
this.exportEditor.update(handleFileNodes, { discrete: true })
})
await new Promise<void>((resolve) => {
await new Promise<void>((resolve, reject) => {
const convertToFormat = () => {
switch (toFormat) {
case 'txt':
@ -164,10 +169,16 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
break
case 'pdf': {
void import('../Lexical/Utils/PDFExport/PDFExport').then(({ $generatePDFFromNodes }): void => {
void $generatePDFFromNodes(this.exportEditor, config?.pdf?.pageSize || 'A4').then((pdf) => {
content = pdf
resolve()
})
void $generatePDFFromNodes(this.exportEditor, config?.pdf?.pageSize || 'A4')
.then((pdf) => {
content = pdf
resolve()
})
.catch((error) => {
didThrow = true
console.error(error)
reject(error)
})
})
break
}
@ -181,7 +192,7 @@ export class HeadlessSuperConverter implements SuperConverterServiceInterface {
this.exportEditor.update(convertToFormat, { discrete: true })
})
if (typeof content !== 'string') {
if (didThrow || typeof content !== 'string') {
throw new Error('Could not export note')
}

Some files were not shown because too many files have changed in this diff Show more