From db16d494528b35ee79d39f3f9d7ffcd47cd3df57 Mon Sep 17 00:00:00 2001 From: Jan Rydzewski Date: Wed, 10 Jan 2024 10:40:39 +0100 Subject: [PATCH 1/3] Add Strong inline element --- example.docx | Bin 846064 -> 846165 bytes example.html | 9 ++++++++- example.pdf | Bin 6839 -> 6943 bytes example.py | 5 +++-- red_tape_kit/ast.py | 15 +++++++++++++++ red_tape_kit/html.py | 7 +++++-- red_tape_kit/pdf.py | 12 +++++++++--- 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/example.docx b/example.docx index 8b60bced19cb88c19f8ae98fb52a5b80e559ce38..0208183c4e0e37a556d6963984dd4bed1edbee64 100644 GIT binary patch delta 394 zcmYMvOG`pQ6bJA-zO7RPE`(B=ooNp%p@nFXASSi&D_pMEoGh?_dvvNILZzbmdTlN1RcvOcNImB8WqNI#t4G0+ePmMz9#5{<54e*dlJa51( zdBRQu=1B#on=nUes5T*Fcs>T9R9vv(V-q}ja~bfq1jA@=@!krCS`gOh5D!|se~r}^ z1j#3w+7Q%DRWRI!K;P&M2zf<~dlQPHVXO^ye7}bQ5dLO|j6dLA8{DSmM>qTi+YyTY delta 246 zcmcb5$@s%X;|(8#IonUY;1@GsU=Z8rcmf#Jr@KGp@nqDWKJh7!4b#d6)6YNU zapMXJ;9)4wFG|rbo}T!OM|!%|Gaf0%tm&c8cpMpnrcZyyW6xZh&NKagI*-`&_s@8= z7_+BKJ?C*?S~Pij%yS+uM%L-epYymf&7C&=-E$s0#<1zSFL-PjCr-}>QcP#IO<(l_ bXxXtn(_et-6Njd&z2wnkYkbVZz`y_iW+Y$? diff --git a/example.html b/example.html index 04f48de..1697c8a 100644 --- a/example.html +++ b/example.html @@ -29,7 +29,14 @@

The first paragraph.

- The second paragraph. + + Kava + + or + + kava kava + + (Piper methysticum: Latin 'pepper' and Latinized Greek 'intoxicating') is a crop of the Pacific Islands.

We allow diff --git a/example.pdf b/example.pdf index 79cf9de1addc2088023afd9c2bd2a3f75bf9b3f4..463919079039d81d7f555d9135a6c5d30d00992a 100644 GIT binary patch delta 2918 zcmai0c|6qVAK%==FvPg82;+#k57)4AwM4lWP1LwH4AvTwugVcxVkkmVD`8!cVN56n zu{$CmgB&?FqBZ3PA&SX8{}nBW!9a9Pwi~*(X?-T9A`tAIgD)la}|O%o2XX&h1C=?Z_6Pu_I%_I<-{wiNtrrML}p;X%UT zt9RwrreQ0OWGoxao_YRSIwMGX2bLaXyEb9^!@~rK*%$=)bRpyZp@-B**gPPHhlHvm zojvOdn4K)lyZddso&M0L<=F zhAhs_ElpL6?DI&PMDDp|i$>5mEss$dMMx?&tZZsQx~VKG^_oRdk=qk$mlLS(eBll0frA?mAC(?QLmsEhfKHVKzoJ$F zJ3)5W%yj5zeN@3SYM@WCD||>?Yv=?2Y|E{F@PM6X^QetM>_-dO=IPpa#AMrdUUTv- z5w8U5g|tUj!x2LsT_qqW`8cTf4$1VXsc`WjBxp+DzNTcI|F*2|{1jaL95a_8U+>PY zw4I+#{m|hOiD4~z-W+=|Hn7Q%Jz{-ETCtnbs(oS<5bZ zIvY5lIx9|AHqJx2f`~?rQ;0QP*8BS~;&E-U^NsJ-0#JHHaeYRYGVZX9y#{9akOYp$%>OIf0VB<^G zt9p8^!AvAqlUcVxT8VkHeiI&ic2ju1x)f+!-UCrHSpP_(OJ=KCrawRxq3haMI1l_v znKc<4kx7L>eRh(!s(yXvXF`c=5F_?LC3f^DlVLPD>Cv6dHeK(e2qqCc-niM zV$(9ksVXfv`f->CA*zw?e~SdTL~g%zUD8^*IHjZ5%v!R|F@<90{+#3NwU4*Yu*gn! z5BuCpTWxyfu^x9`k-wLj)sWI~4te77#C#p*T}_W(hd%g2W-v4#yu#R>5)X?odlVbJ z?z)l^oXDw2Gb4?*aOI4odMgEHB%lz(dUv?4Rn6m{e;^WXUKz<(~)|6l(;Bjb9Jo z8>VV&^4>|nx428UOWGe*pg^aao$!7IZ55iU*IynT_$V->Ti9vWiuPoN8H}p_2@H|PRg*ga{i5& zWbu3K{*42_?xA+d*k+P01FO#_OLxZLy~Kd!noY zu6xl7lNowzQXP?@qx)2?Yi_hYkxl@O`^Q5Vne!yb%N%TVk;uxnm5BaG4B`ci1aI<# z8$_<26G~#n)lc&A7Y9wUP4spO9T@dXq1cDjZ*U}o#Z4pqv6?N0adqgeMRiffGfQIG z4l2W?21UcOvoiy7vGXNf8H=;yJ1-a2^r~Z5-nAL7>QL%|mBHwr91EzfBkt5CW5Zkr zW1YJ)XXHbtDAdVgQn@14g?+ozc1+!FZD!;d$Xz?8POj91QJ)}n%|Gu=8TaJ;YWzrt zf1FQZM&}QTHP4HZ4oL${qh-O^wbBqXmdX}4a88Csbm7(JHf}i(&USUwUACIKxG%*U zuvQu{$x;EfxVC)VZyk`WurCbbjORt?2~n$EPk^lCmmTvINodQ~XD8}Unqo*5H)cN} zT-Tm^G}*JFb%OX>fwI;`7<1jVT~;YFSvFwrzT;+15F5N6(TMEurD=zleRu50+l+|D z+E26zB72wp#culB?%cB^j70(tpS4PS1-qxj=ulu_$2!T3M(o*TZZGbso8Ne@LXJDN zx51T|Vz$DPo0kmA_L}2P_W>m5o<1^HW}ub0J+n8==}Xq2?}zX6%Is8)*SUy61bC1L znx9k+4+=?>g;IIoNLm&&hX;YCX-lUa!qz>OPCE4Y2c!!BGE_rUh5s_(NH`MnZw!S% zp$=fkgIW}ha9}+Y0g3##7X3NOUl{OJU^D`GAP5@yUl;*@U_mteKQR1341*_ptp&c+ zjK&_!8jbrW@h={p@ORcjGz Y_=NvR4JZ2vV-aYAu!e@EgO%|A02E;ECjbBd delta 2826 zcmai#cTiL58i#=hNT@2+rLR(?29lFR5TpuHFN*ZgM3E*znos(VvzO^rUMbRrUgPc%O9TU9EG{Jy&QS9&RiQ}>fcvmwscvk#dZ>ko-g9K%J%!; ze*7LgB}>@|-1sgm>;jo#tiUxrMVx$|aOgpt=>f&=lLvA$4ZJ|&Y`k3enPMqP%)Q}W zw`;s>psHFPnGq^d$vaZ>>(i=;5W6t;+=W)l61WitD!$Jy{y>^ri>!D!F0*1EsH|&! zv)xQJ(TX<`n2?_&#$)ve83)^kL5f^Q^qPk}=V|vBU7nv&JFi@I-c>+`rFmyRrNH?yl!#|5QQQqKCn2I| z)z#n3ht+4UCYK9HI5!i@9W|?Eil2)u09MVTeCG=WHV0Y$Hwut`%2_ZBrcETM-X^5Y zZf3ZcdmEf;;miGOi);wtjm@OUD?e1#dl#=Zum6(k`ss5<(Rk_C?U!BGW!9PDDllfC zd+&_@um;9n-oZb7us^1yPkJ?LfJ+_rW0~5)7P9neRmZK>BSf&SC^APiQOQArwIPT9 zEe=quy;YSDXm8QcTJ+YD(?xTvbHxSQ5-6u93h-#Hqqa(wjE2MpIjuIYbT_9EyY1*k z8n_$vus?wfVW3Z%u#R`WHM9{Pmf3yH;;(+>!^EH9e+{Bhz~4XI0uKkOl~~&JmPDhD z#t%Qw?*t$UB_P+~8aZ$cHj47pH%iq_!lrYIRL>ZR*Ts9t3^h)4KJt z_5<|QvI?@#hQn@e9h4fB9jRBmFyzfVA=j&QfbWJ(+AoHT+QKx zMo%60PWR2L8@cZtJ;sUN+IBhD#=ajmJ7#~-H_sWH9^Q11C{huwwV!sXD!f*}+29yc zO7r;Xfbc-4tFb4>Vb+)UWgmdbm5P>26|xJrU*}3k)j)e~-^k^g$$fhCld8q`kK)u6 zYH3F7NNxa8EcE$-U8xjQZrk^RuD9E}<_E6uuGMH48dKN}b(liubHruV!b*(v1)z#t z-80v|Q|~Kf_GCmtNWJj17{u^Hnu=(pl5BkHe8a>gDYGLZtbNx`nfmiL)fQ zPmrPE6W|`D;ki##cq45FG_xDupwvDtYVr_`_t2!eJW%nQNL%hmupD}L+a*Axx`tG` z5JDH~2#Loq)4ylP)hz@x*6Ez&07t4ZRWS8l% z<*=nCGO5+iFQJ={WmxUuDiQGNH#d(ln&a5;=4{Xx=;XbSX3tyTq}^M(;M?DwxnqNN zS28!zif+d3d>W}QTDm_sI@geUx9gjR=8S#Z%J`2mT?}`&%9CoUZ=3U3UBrzFK<*L9*-zM<$T@Kax$$HRSp~!SAfO5PSF)2GJ;3-qiL$$p@RU)0~6=0kd*=Pejcv{t+DCx}pu994C=J9F_c`Tu2h|TnFPsT zT*a5sKVP`My`8YO^-Eai3{g1l@xbVIM04T@*X-=l$t}8X!sa^Gt8`ksq&o)FPbXle z+KoCi%UAJ8jGU_~hBsFdEHxmDWE~McW(-@>r+&|%5c=5!$q&4nRrrTUhRp&=Td9k9+L7hcoa_g7F{2{B4e3N<<{AsKt z2li{>icRX2@etE*^-+*wj{<|O&cAp&78q8GU%u?=TruCfER|7|b zZQ(U(b?j@qM?@dt$P|@hd-D0ji>s>Wcb+UdTVVEFA=6sqnIt%}qZ$XXXQMJEvW(|h z)Os4L=Z^QJ_sC9Pflc&kpBu@GX}zp4z*oe?iXgx0eRrnse4VBKoC%$38LdlZI`gc1 z*OTzZ{gkGK$BaCoWEJqIt*i9kcTS}!?onp>b~UTj2uGXDw%U3(3o5Y$6F?Syt&eqc z0&L#pj<)-`r@mc8l#fRrz>~zE@W7GyB#Bra7!toNk 'InlineElement': def plain_string(self) -> str: raise NotImplementedError + def __add__(self, other: 'InlineElement') -> 'InlineSequence': + return InlineSequence(items=[self, other]) + @dataclass class Document: @@ -263,6 +266,18 @@ def plain_string(self) -> str: return self.text.plain_string +@dataclass(frozen=True) +class Strong(InlineElement): + text: InlineElement + + def normalized(self) -> 'Strong': + return Strong(text=normalized_inline(self.text)) + + @property + def plain_string(self) -> str: + return self.text.plain_string + + def normalized_inline(element: InlineElement) -> InlineElement: if isinstance(element, str): return Text(text=element).normalized() diff --git a/red_tape_kit/html.py b/red_tape_kit/html.py index a87f05a..718e916 100644 --- a/red_tape_kit/html.py +++ b/red_tape_kit/html.py @@ -2,8 +2,8 @@ from base64 import b64encode from .ast import ( - Attachment, DefinitionList, Image, InlineSequence, Paragraph, Section, Sequence, Table, TableCellSpan, Text, - UnorderedList, + Attachment, DefinitionList, Image, InlineSequence, Paragraph, Section, Sequence, Strong, Table, TableCellSpan, + Text, UnorderedList, ) @@ -131,6 +131,9 @@ def add_inline_element(self, html_el, element): self.add_inline_element(html_el, sub_element) elif isinstance(element, Attachment): self.add_attachment(html_el, element) + elif isinstance(element, Strong): + strong = ET.SubElement(html_el, 'strong') + self.add_inline_element(strong, element.text) else: raise ValueError(f'Unknown inline element type {type(element)}') diff --git a/red_tape_kit/pdf.py b/red_tape_kit/pdf.py index 6641f9e..021adc7 100644 --- a/red_tape_kit/pdf.py +++ b/red_tape_kit/pdf.py @@ -1,10 +1,10 @@ from logging import getLogger -from fpdf import FPDF, TitleStyle, XPos, YPos +from fpdf import FPDF, FontFace, TitleStyle, XPos, YPos from .ast import ( - Attachment, DefinitionList, Image, InlineSequence, Paragraph, Section, Sequence, Table, TableCellSpan, Text, - UnorderedList, + Attachment, DefinitionList, Image, InlineSequence, Paragraph, Section, Sequence, Strong, Table, TableCellSpan, + Text, UnorderedList, ) @@ -184,6 +184,8 @@ def add_inline_element(self, inline_element): return self.add_inline_sequence(inline_element) elif isinstance(inline_element, Attachment): return self.add_attachment(inline_element) + elif isinstance(inline_element, Strong): + return self.add_strong(inline_element) else: raise ValueError(f'Unknown inline element type {inline_element}') @@ -216,6 +218,10 @@ def add_attachment(self, attachment): ) return True + def add_strong(self, strong): + with self.use_font_face(FontFace(emphasis="BOLD")): + return self.add_inline_element(strong.text) + def header(self): pass From 0a8ab7c479dcbbed3df77b8405fd65a7165acd1c Mon Sep 17 00:00:00 2001 From: Jan Rydzewski Date: Wed, 10 Jan 2024 11:06:43 +0100 Subject: [PATCH 2/3] Improve PDF sequence spacing --- red_tape_kit/pdf.py | 14 +++++++++----- tests/test_renderers.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/red_tape_kit/pdf.py b/red_tape_kit/pdf.py index 021adc7..413fc3f 100644 --- a/red_tape_kit/pdf.py +++ b/red_tape_kit/pdf.py @@ -19,7 +19,10 @@ class FPDFRenderer(FPDF): MARGIN_RIGHT = 20 # mm MARGIN_TOP = 20 # mm MARGIN_BOTTOM = 20 # mm + SEQUENCE_SPACE = 7 # mm + UNORDERED_LIST_HSPACE = 7 # mm UNORDERED_LIST_BULLET = '-' + UNORDERED_LIST_BULLET_SPACE = 5 # mm def __init__(self, document, **kwargs): super().__init__(**kwargs, unit='mm', format='A4') @@ -114,7 +117,7 @@ def add_sequence(self, sequence, level): ln_required = False for sub_element in sequence.items: if ln_required: - self.ln(7) + self.ln(self.SEQUENCE_SPACE) ln_required = self.add_element(sub_element, level) if ln_required: anything_generated = True @@ -125,7 +128,8 @@ def add_section(self, section, level): return self.add_element(section.body, level + 1) def add_paragraph(self, paragraph): - return self.add_inline_element(paragraph.text) + self.add_inline_element(paragraph.text) + return True def add_table(self, table_data): with self.table( @@ -150,11 +154,11 @@ def add_elementary_table(self, pdf_table, elementary_table): def add_unordered_list(self, unordered_list): orig_left_margin = self.l_margin - new_left_margin = orig_left_margin + 5 + new_left_margin = orig_left_margin + self.UNORDERED_LIST_BULLET_SPACE self.set_left_margin(new_left_margin) for i, item in enumerate(unordered_list.items): if i > 0: - self.ln(7) + self.ln(self.UNORDERED_LIST_HSPACE) with self.unbreakable() as self: self.set_x(orig_left_margin) self.cell(text=self.UNORDERED_LIST_BULLET) @@ -191,7 +195,7 @@ def add_inline_element(self, inline_element): def add_text(self, text): self.write_lh(text.text) - return text != '' + return text.text != '' def add_inline_sequence(self, inline_sequence): anything_generated = False diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 42cabf6..4b60b9d 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -142,6 +142,33 @@ def test_not_dependent_on_current_date(render): assert a == b +@pytest.mark.xfail(reason='Preserving final ln in fpdf is hard') +def test_empty_paragraph_at_the_end_is_represented(render): + doc_a = DocumentFactory( + body=Sequence([ + Paragraph(text=Text('A')), + Paragraph(text=Text('')), + ]), + ) + doc_b = DocumentFactory( + body=Paragraph(text=Text('A')), + ) + assert render(doc_a) != render(doc_b) + + +def test_empty_paragraph_is_represented(render): + doc_a = DocumentFactory( + body=Sequence([ + Paragraph(text=Text('')), + Paragraph(text=Text('A')), + ]), + ) + doc_b = DocumentFactory( + body=Paragraph(text=Text('A')), + ) + assert render(doc_a) != render(doc_b) + + def test_empty_attachment_no_smoke(render): doc = DocumentFactory( body=Paragraph(text=Attachment( From c73f2825befd88b244b053f28f14f857ea4caad2 Mon Sep 17 00:00:00 2001 From: Jan Rydzewski Date: Wed, 10 Jan 2024 11:07:11 +0100 Subject: [PATCH 3/3] Version 0.5.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9ecb4d2..d7a86c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "red-tape-kit" -version = "0.5.0" +version = "0.5.1" description = "" authors = ["Your Name "] readme = "README.md"