legalmd/legalmd/latex_renderer.py

322 lines
12 KiB
Python

# legalmd: Markdown-based legal markup
# Copyright © 2019 Lee Yingtong Li (RunasSudo)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import hashlib
import mistletoe
import mistletoe.latex_renderer
LMARG = '1cm'
def format(fstr, *args, **kwargs):
fstr2 = fstr
fstr2 = fstr2.replace('\\N', '\n')
fstr2 = fstr2.replace('{', '{{')
fstr2 = fstr2.replace('}', '}}')
fstr2 = fstr2.replace('<', '{')
fstr2 = fstr2.replace('>', '}')
kwargs['lmarg'] = LMARG
return fstr2.format(*args, **kwargs)
class LaTeXRenderer(mistletoe.latex_renderer.LaTeXRenderer):
def __init__(self, *extras):
#super().__init__(*extras)
# Omit LaTeX tokens
self.packages = {}
mistletoe.base_renderer.BaseRenderer.__init__(self, *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
self.render_map['Note'] = self.render_note
self.render_map['Definition'] = self.render_definition
self.heading_last = False
def render_raw_text(self, token):
result = super().render_raw_text(token)
result = result.replace('★★★', r'\texorpdfstring{\freeserif ★★★}{★★★}')
return result
def render_link(self, token):
template = '\\href{{{target}}}{{{inner}}}'
inner = self.render_inner(token)
return template.format(target=token.target, inner=inner)
def render_auto_link(self, token):
return '\\url{{{}}}'.format(token.target)
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>}{<reference_type><reference_num>}',
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
return format(r'{\par\vspace{1cm plus 0.3cm minus 0.1cm}\bfseries\fontsize{13pt}{15pt}\selectfont\centering\uppercase{<content>}\phantomsection\addcontentsline{toc}{section}{<content>}\nopagebreak\par}',
content=self.render_inner(token)
)
if token.level == 2:
heading_last, self.heading_last = self.heading_last, True
return format(r'{\par\vspace{<space_above>}\bfseries\fontsize{13pt}{15pt}\selectfont\centering <content>\phantomsection\addcontentsline{toc}{subsection}{<content>}\nopagebreak\par}',
space_above='1cm plus 0.3cm minus 0.1cm' if not heading_last else '0cm',
content=self.render_inner(token)
)
if token.level == 3:
heading_last, self.heading_last = self.heading_last, False
return format(r'{\par\leftskip=<lmarg>\bfseries <content>\phantomsection\addcontentsline{toc}{subsubsection}{<content>}\nopagebreak\par}',
content=self.render_inner(token)
)
if token.level == 4:
heading_last, self.heading_last = self.heading_last, False
return format(r'{\par\leftskip=<lmarg>\itshape <content>\nopagebreak\par}',
content=self.render_inner(token)
)
def render_numbered_heading(self, token):
if token.full_label():
sha = hashlib.sha256()
sha.update(token.full_label().encode('utf-8'))
hyperlink = format(r'\makebox[0pt]{\hypertarget{<linkname>}{}}',
linkname=sha.hexdigest()
)
else:
hyperlink = '{}'
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.1cm}<hyperlink>\bfseries\fontsize{13pt}{15pt}\selectfont\centering\uppercase{Part <label>—<content>}\phantomsection\addcontentsline{toc}{section}{Part <label>—<content>}\nopagebreak\par}',
hyperlink=hyperlink,
label=token.label,
content=self.render_inner(token)
)
if token.level == 2:
# Division
heading_last, self.heading_last = self.heading_last, True
return format(r'{\par\vspace{<space_above>}<hyperlink>\bfseries\fontsize{13pt}{15pt}\selectfont\centering Division <label>—<content>\phantomsection\addcontentsline{toc}{subsection}{Division <label>—<content>}\nopagebreak\par}',
hyperlink=hyperlink,
space_above='1cm plus 0.3cm minus 0.1cm' if not heading_last else '0cm',
label=token.label,
content=self.render_inner(token)
)
if token.level == 3:
# Section
heading_last, self.heading_last = self.heading_last, False
return format(r'{<pagebreak>\par<hyperlink>\leftskip=\quotemargin\bfseries\makebox[<lmarg>][l]{<label>}<content>\phantomsection\addcontentsline{toc}{subsubsection}{\protect\numberline{<label>} <content>}\nopagebreak\par}',
pagebreak=r'\pagebreak[3]' if not heading_last else '',
hyperlink=hyperlink,
label=token.label,
content=self.render_inner(token)
)
def render_paragraph(self, token):
self.heading_last = False
content = self.render_inner(token)
return format(r'\N{\par\leftskip=\dimexpr\quotemargin+<lmarg>\relax <content><pagebreak>\par}\N',
content=content,
pagebreak=r'\nopagebreak' if content.strip().endswith(':') else ''
)
def render_quote(self, token):
return format(r'{\quotemargin=<lmarg>\renewcommand{\addcontentsline}[3]{}<content>}',
content=self.render_inner(token)
)
def render_definition(self, token):
self.heading_last = False
return format(r'\N{\par\leftskip=\dimexpr<lmarg>+<level>cm\relax\hangindent=1cm <content>\par}\N',
level=token.level,
content=self.render_inner(token)
)
def render_subrules(self, token):
self.heading_last = False
return format(r'\N{<content>}\N',
content=self.render_inner(token)
)
def render_subrules_item(self, token):
content = self.render_inner(token)
if token.label:
if token.full_label():
sha = hashlib.sha256()
sha.update(token.full_label().encode('utf-8'))
hyperlink = format(r'\makebox[0pt]{\hypertarget{<linkname>}{}}',
linkname=sha.hexdigest()
)
else:
hyperlink = '{}'
return format(r'\N{\par<hyperlink>\leftskip=\dimexpr\quotemargin+<lmarg>+<level>cm\relax\hangindent=1cm\parskip=<parskip>\makebox[<lmarg>][l]{<label>}<content><pagebreak>\par}\N',
hyperlink=hyperlink,
parskip=r'\parskip',# if token.level <= 1 else '0cm',
label=token.label,
level=token.level,
content=content,
pagebreak=r'\nopagebreak' if content.strip().endswith(':') else ''
)
else:
return format(r'\N{\par\leftskip=\dimexpr<lmarg>+<level>cm+1cm\relax\parskip=<parskip> <content><pagebreak>\par}\N',
parskip=r'\parskip',# if token.level <= 1 else '0cm',
level=token.level,
content=content,
pagebreak=r'\nopagebreak' if content.strip().endswith(':') else ''
)
def render_note(self, token):
self.heading_last = False
return format(r'\N{\nopagebreak\par\footnotesize\selectfont\settowidth{\notetaglength}{\bfseries <label>:}\addtolength{\notetaglength}{1em}\leftskip=\dimexpr<lmarg>+<level>cm\relax\hangindent=\notetaglength\makebox[\notetaglength][l]{\bfseries <label>:}<content>\par}\N',
level=token.level,
label=token.label,
content=self.render_inner(token)
)
def render_table(self, token):
align = [r'l@{\hspace{1ex}}'] + ['X' for col in token.column_align]
if hasattr(token, 'header'):
for i, col in enumerate(token.header.children):
align[i+1] += '[{}]'.format(col.weight)
result = [
format('{\\footnotesize\\begin{longtabu} to \\dimexpr\\columnwidth-<lmarg>\\relax [r] {'),
' '.join(align),
'}\\toprule\n']
if hasattr(token, 'header'):
result.append(self.render_table_header(token.header) + '\\midrule[\\heavyrulewidth]\n')
for i, row in enumerate(token.children):
result.append(self.render_table_row(row))
if i != len(token.children) - 1:
result.append('\\midrule\n')
result.append(r'\bottomrule\end{longtabu}}')
return ''.join(result)
def render_table_header(self, token):
cells1 = []
cells2 = []
for cell in token.children:
if cell.colnum == 0:
coldef = format(r'p{\dimexpr\linewidth-1cm-1ex-<colx>\tabucolX-<colnd>\tabcolsep\relax}',
colx=sum(col.weight for col in token.children[1:]),
colnd=2*len(token.children)
)
cells1.append(format('\\multicolumn{2}{<coldef>}{\\bfseries Column <label>}',
coldef=coldef,
label=cell.label
))
cells2.append(format('\\multicolumn{2}{<coldef>}{\\bfseries <content>}',
coldef=coldef,
content=self.render_inner(cell)
))
else:
cells1.append('{\\bfseries Column ' + cell.label + '}')
cells2.append('{\\bfseries ' + self.render_inner(cell) + '}')
return ' & '.join(cells1) + ' \\\\\\midrule\n' + ' & '.join(cells2) + ' \\\\'
def render_table_row(self, token):
cells = [(token.children[0].label + '.') if token.children[0].label else ''] + [self.render_table_cell(child) for child in token.children]
return ' & '.join(cells) + ' \\strut\\\\'
def render_table_cell(self, token):
return self.render_inner(token)
def render_document(self, token):
template = r'''
\documentclass[a4paper,12pt]{article}
<packages>
% Configuration
\usepackage[top=1.25cm,bottom=1.13cm,inner=2cm,outer=2cm,headheight=8pt,headsep=0.5cm,footskip=1cm,includehead,includefoot]{geometry}
\frenchspacing
\usepackage[hidelinks,bookmarksnumbered=true,unicode,pdftitle={<title>},pdfauthor={https://gitlab.com/RunasSudo/legalmd}]{hyperref}
\usepackage{bookmark} % Non-sequential bookmarks
\usepackage{parskip}
\setlength{\parskip}{0.35cm plus 0.1cm minus 0.1cm}
\usepackage{fancyhdr}
\setlength{\emergencystretch}{3em}
\usepackage{microtype}
\usepackage{longtable}\usepackage{tabu}\usepackage{booktabs}
\newlength\quotemargin
\newlength\notetaglength
% TOC format
\usepackage{titletoc}
\makeatletter
\newcounter{toc@section} % For some reason \newif doesn't work here
\newcommand{\toc@sectiontrue}{\setcounter{toc@section}{1}}
\newcommand{\toc@sectionfalse}{\setcounter{toc@section}{0}}
\titlecontents{section}[0pt]{\vspace{0.21cm}\toc@sectionfalse\bfseries}{}{\uppercase}{\titlerule*[1pc]{.}\contentspage}[]
\titlecontents{subsection}[0pt]{\vspace{0.21cm}\bfseries\scshape}{}{\toc@sectionfalse}{\titlerule*[1pc]{.}\contentspage}[]
\dottedcontents{subsubsection}[\dimexpr<lmarg>+0.5cm\relax]{\ifnum\value{toc@section}=0\vspace{0.21cm}\fi\toc@sectiontrue}{1cm}{1pc}[]
\makeatother
% Fonts
\usepackage[math-style=ISO, bold-style=ISO]{unicode-math}
\setmainfont[RawFeature=-tlig]{TeX Gyre Termes}
\setsansfont[RawFeature=-tlig]{TeX Gyre Heros}
\setmonofont[RawFeature=-tlig]{TeX Gyre Cursor}
\setmathfont[RawFeature=-tlig]{TeX Gyre Termes Math}
\newfontfamily{\freeserif}{FreeSerif}[RawFeature=-tlig]
\renewcommand{\familydefault}{\sfdefault}
% Front matter
\begin{document}
\fancypagestyle{plain}{\fancyhf{}}
\pagestyle{fancy}\fancyhf{}\renewcommand{\headrulewidth}{0pt}
\lhead{\textsf{\scriptsize <title>}}\rhead{\textsf{\scriptsize <author>}}\lfoot{\textsf{\scriptsize <footer>}}\rfoot{\textsf{\scriptsize\thepage}}
\pagenumbering{roman}
{\bfseries\centering\fontsize{13pt}{15pt}\selectfont\uppercase{<title>}\par INDEX\par}
\makeatletter\@starttoc{toc}\makeatother
\newpage\pagenumbering{arabic}
% Content
<inner>
\end{document}'''
self.footnotes.update(token.footnotes)
return format(template,
inner=self.render_inner(token),
packages=self.render_packages(),
title=token.title,
author=token.author,
footer=token.footer
)