|
@@ -35,6 +35,9 @@ function tokenize(html: string): Token[] {
|
|
|
return tokens
|
|
return tokens
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** Tags drawn as their own Text or View; everything else sits inside a line of text. */
|
|
|
|
|
+const BLOCK_TAGS = new Set(['p', 'h2', 'h3', 'ul', 'ol', 'li', 'blockquote'])
|
|
|
|
|
+
|
|
|
interface ASTNode {
|
|
interface ASTNode {
|
|
|
tag: string
|
|
tag: string
|
|
|
children: (ASTNode | string)[]
|
|
children: (ASTNode | string)[]
|
|
@@ -93,9 +96,43 @@ export function HtmlToPdf({
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Children of a View. react-pdf draws text only inside a Text, so a run of
|
|
|
|
|
+ * loose words, bold or line breaks between the blocks goes into one. Notes
|
|
|
|
|
+ * written as plain lines with <br> and no <p> (as AI Write saves them)
|
|
|
|
|
+ * printed only their line breaks before: an empty, page-long Notes box.
|
|
|
|
|
+ */
|
|
|
|
|
+ function renderBlocks(nodes: (ASTNode | string)[]): React.ReactNode[] {
|
|
|
|
|
+ const out: React.ReactNode[] = []
|
|
|
|
|
+ let run: (ASTNode | string)[] = []
|
|
|
|
|
+ const flush = () => {
|
|
|
|
|
+ // Whitespace between two blocks was never drawn and still is not.
|
|
|
|
|
+ if (run.some((n) => typeof n !== 'string' || n.trim() !== '')) {
|
|
|
|
|
+ out.push(
|
|
|
|
|
+ <Text key={nextKey()} style={base}>
|
|
|
|
|
+ {renderNodes(run)}
|
|
|
|
|
+ </Text>
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ run = []
|
|
|
|
|
+ }
|
|
|
|
|
+ for (const node of nodes) {
|
|
|
|
|
+ if (typeof node !== 'string' && BLOCK_TAGS.has(node.tag)) {
|
|
|
|
|
+ flush()
|
|
|
|
|
+ out.push(renderNode(node))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ run.push(node)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ flush()
|
|
|
|
|
+ return out
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function renderNode(node: ASTNode): React.ReactNode {
|
|
function renderNode(node: ASTNode): React.ReactNode {
|
|
|
const k = nextKey()
|
|
const k = nextKey()
|
|
|
- const children = renderNodes(node.children)
|
|
|
|
|
|
|
+ // A quote lays its children out as blocks itself; rendering them here too
|
|
|
|
|
+ // would draw every nested quote twice, and twice again per level.
|
|
|
|
|
+ const children = node.tag === 'blockquote' ? [] : renderNodes(node.children)
|
|
|
|
|
|
|
|
switch (node.tag) {
|
|
switch (node.tag) {
|
|
|
case 'p':
|
|
case 'p':
|
|
@@ -196,7 +233,7 @@ export function HtmlToPdf({
|
|
|
opacity: 0.8,
|
|
opacity: 0.8,
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
- {children}
|
|
|
|
|
|
|
+ {renderBlocks(node.children)}
|
|
|
</View>
|
|
</View>
|
|
|
)
|
|
)
|
|
|
case 'br':
|
|
case 'br':
|
|
@@ -206,5 +243,5 @@ export function HtmlToPdf({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return <View>{renderNodes(ast)}</View>
|
|
|
|
|
|
|
+ return <View>{renderBlocks(ast)}</View>
|
|
|
}
|
|
}
|