From 04a7e52f2e720dbd1e5df8a16ed225e322a934ac Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sat, 14 Sep 2019 23:02:14 +1000 Subject: [PATCH] Implement cross-references --- example.md | 2 +- example.pdf | Bin 12964 -> 13113 bytes example.rtf | 2 +- legalmd/__main__.py | 18 ++++++++++ legalmd/latex_renderer.py | 42 ++++++++++++++++++++--- legalmd/legal_token.py | 69 ++++++++++++++++++++++++++++++++++++++ legalmd/rtf_renderer.py | 8 +++++ 7 files changed, 135 insertions(+), 6 deletions(-) diff --git a/example.md b/example.md index 3b36983..6c4ae75 100644 --- a/example.md +++ b/example.md @@ -22,7 +22,7 @@ This is the *blah blah*. hijkl - Continued + Cross reference to `rule 1`_ (2) DEF diff --git a/example.pdf b/example.pdf index 50fdf72d092dad6d1a105a6374b1ada545bab44d..96b89b8847166af4b0f3215ad46b7475e20b8282 100644 GIT binary patch delta 3235 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 AV*mgE delta 3077 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 Fe*pR!llA}r diff --git a/example.rtf b/example.rtf index 157b11c..14ceecb 100644 --- a/example.rtf +++ b/example.rtf @@ -1 +1 @@ -{\rtf1\deff0{\fonttbl{\f0 TeX Gyre Heros{\*\falt FreeSans}{\*\falt Liberation Sans}{\*\falt Arial};}}\paperw11905\paperh16837\margl1133\margr1133\margt1133\margb1133{\header\f0\fs16\tqr\tx9637 Example Legal Document\tab }{\footer\f0\fs16\tqr\tx9637 \tab\chpgn}\sa198\fs1\~\fs24 {\fs20\li566{\b Note:} This is a note.\par}{\sb566\keepn\b\fs26\qc\caps Part 1\u8212?Preliminary\par}{\keepn\b\fi-566\li566 1\tab Short title\par}{\li566 This is the {\i blah blah}.\par}{\keepn\b\fi-566\li566 2\tab Another heading\par}{\fi-566\li1133 (1)\tab ABC\par}{\li1133 Continued\par}{\fi-566\li1700 (a)\tab asdf\par}{\fi-566\li1700 (b)\tab abcdefg\par}{\li1700 hijkl\par}{\li1133 Continued\par}{\fi-566\li1133 (2)\tab DEF\par}{\keepn\b\fi-566\li566 3\tab Table example\par}{\sa0\trowd\trgaph120\trleft566\clbrdrt\brdrs\clbrdrb\brdrs\cellx2380\clbrdrt\brdrs\clbrdrb\brdrs\cellx9636 {\b Column 1\intbl\cell}{\b Column 2\intbl\cell}\row{\b This is a long column header that will overflow onto multiple lines\intbl\cell}{\b Second column\intbl\cell}\row {1. First row\intbl\cell}{Foo bar\intbl\cell}\row {2. Second row\intbl\cell}{Baz qux\intbl\cell}\row }} +{\rtf1\deff0{\fonttbl{\f0 TeX Gyre Heros{\*\falt FreeSans}{\*\falt Liberation Sans}{\*\falt Arial};}}\paperw11905\paperh16837\margl1133\margr1133\margt1133\margb1133{\header\f0\fs16\tqr\tx9637 Example Legal Document\tab }{\footer\f0\fs16\tqr\tx9637 \tab\chpgn}\sa198\fs1\~\fs24 {\fs20\li566{\b Note:} This is a note.\par}{\sb566\keepn\b\fs26\qc\caps Part 1\u8212?Preliminary\par}{\keepn\b\fi-566\li566 1\tab Short title\par}{\li566 This is the {\i blah blah}.\par}{\keepn\b\fi-566\li566 2\tab Another heading\par}{\fi-566\li1133 (1)\tab ABC\par}{\li1133 Continued\par}{\fi-566\li1700 (a)\tab asdf\par}{\fi-566\li1700 (b)\tab abcdefg\par}{\li1700 hijkl\par}{\li1133 Cross reference to rule\u160?1\par}{\fi-566\li1133 (2)\tab DEF\par}{\keepn\b\fi-566\li566 3\tab Table example\par}{\sa0\trowd\trgaph120\trleft566\clbrdrt\brdrs\clbrdrb\brdrs\cellx2380\clbrdrt\brdrs\clbrdrb\brdrs\cellx9636 {\b Column 1\intbl\cell}{\b Column 2\intbl\cell}\row{\b This is a long column header that will overflow onto multiple lines\intbl\cell}{\b Second column\intbl\cell}\row {1. First row\intbl\cell}{Foo bar\intbl\cell}\row {2. Second row\intbl\cell}{Baz qux\intbl\cell}\row }} diff --git a/legalmd/__main__.py b/legalmd/__main__.py index da86322..11c3f11 100644 --- a/legalmd/__main__.py +++ b/legalmd/__main__.py @@ -40,4 +40,22 @@ with renderer_cls() as renderer: doc.author = rawdoc.get('author', '') doc.footer = rawdoc.get('footer', '') + doc.full_label_map = {} + + # Preprocess custom tokens + def walk_elem(elem, parent=None): + elem.doc = doc + elem.parent = parent + + if isinstance(elem, legal_token.NumberedHeading) or isinstance(elem, legal_token.SubrulesItem): + full_label = elem.full_label() + if full_label: + doc.full_label_map[full_label] = elem + + if hasattr(elem, 'children'): + for child in elem.children: + walk_elem(child, elem) + + walk_elem(doc) + print(renderer.render(doc)) diff --git a/legalmd/latex_renderer.py b/legalmd/latex_renderer.py index d78f5ff..19345a4 100644 --- a/legalmd/latex_renderer.py +++ b/legalmd/latex_renderer.py @@ -14,6 +14,8 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import hashlib + import mistletoe import mistletoe.latex_renderer @@ -35,6 +37,8 @@ class LaTeXRenderer(mistletoe.latex_renderer.LaTeXRenderer): def __init__(self, *extras): super().__init__(*extras) + self.render_map['CrossReference'] = self.render_cross_reference + self.render_map['NumberedHeading'] = self.render_numbered_heading self.render_map['Subrules'] = self.render_subrules self.render_map['SubrulesItem'] = self.render_subrules_item @@ -48,6 +52,20 @@ class LaTeXRenderer(mistletoe.latex_renderer.LaTeXRenderer): result = result.replace('★★★', r'\texorpdfstring{\freeserif ★★★}{★★★}') return result + def render_cross_reference(self, token): + reference = token.get_reference() + if not reference: + raise Exception('Unable to resolve reference "{}"'.format(token.reference_num)) + + sha = hashlib.sha256() + sha.update(reference.full_label().encode('utf-8')) + + return format(r'\hyperlink{}{}', + linkname=sha.hexdigest(), + reference_type=(token.reference_type + '~') if token.reference_type else '', + reference_num=token.reference_num + ) + def render_heading(self, token): if token.level == 1: heading_last, self.heading_last = self.heading_last, True @@ -69,10 +87,17 @@ class LaTeXRenderer(mistletoe.latex_renderer.LaTeXRenderer): ) def render_numbered_heading(self, token): + sha = hashlib.sha256() + sha.update(token.full_label().encode('utf-8')) + hyperlink = format(r'\hypertarget{}{}', + linkname=sha.hexdigest() + ) + if token.level == 1: # Part heading_last, self.heading_last = self.heading_last, True - return format(r'{\par\vspace{1cm plus 0.3cm minus 0.3cm}\bfseries\fontsize{13pt}{15pt}\selectfont\centering\uppercase{Part