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

View File

@@ -17,6 +17,7 @@ import {
PageProps, PageProps,
} from '@react-pdf/renderer' } from '@react-pdf/renderer'
import { expose } from 'comlink' import { expose } from 'comlink'
import { FontFamily, registerPDFFonts } from './FontConfig'
export type PDFDataNode = 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() return pdf(<PDFDocument pageSize={pageSize} nodes={nodes} />).toBlob()
} }

View File

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

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