296 lines
11 KiB
Python
296 lines
11 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)
|
|
|
|
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_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.3cm}\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.3cm' 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):
|
|
sha = hashlib.sha256()
|
|
sha.update(token.full_label().encode('utf-8'))
|
|
hyperlink = format(r'\hypertarget{<linkname>}{}',
|
|
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}<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.3cm' 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'{\par<hyperlink>\leftskip=\quotemargin\bfseries\makebox[<lmarg>][l]{<label>}<content>\phantomsection\addcontentsline{toc}{subsubsection}{\protect\numberline{<label>} <content>}\nopagebreak\par}',
|
|
hyperlink=hyperlink,
|
|
label=token.label,
|
|
content=self.render_inner(token)
|
|
)
|
|
|
|
def render_paragraph(self, token):
|
|
self.heading_last = False
|
|
|
|
return format(r'\N{\par\leftskip=\dimexpr\quotemargin+<lmarg>\relax <content>\par}\N',
|
|
content=self.render_inner(token)
|
|
)
|
|
|
|
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>+1cm\relax\hangindent=1cm <content>\par}\N',
|
|
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):
|
|
if token.label:
|
|
sha = hashlib.sha256()
|
|
sha.update(token.full_label().encode('utf-8'))
|
|
hyperlink = format(r'\hypertarget{<linkname>}{}',
|
|
linkname=sha.hexdigest()
|
|
)
|
|
|
|
return format(r'\N{\par<hyperlink>\leftskip=\dimexpr\quotemargin+<lmarg>+<level>cm\relax\hangindent=1cm\parskip=<parskip>\makebox[<lmarg>][l]{<label>}<content>\par}\N',
|
|
hyperlink=hyperlink,
|
|
parskip=r'\parskip',# if token.level <= 1 else '0cm',
|
|
label=token.label,
|
|
level=token.level,
|
|
content=self.render_inner(token)
|
|
)
|
|
else:
|
|
return format(r'\N{\par\leftskip=\dimexpr<lmarg>+<level>cm+1cm\relax\parskip=<parskip> <content>\par}\N',
|
|
parskip=r'\parskip',# if token.level <= 1 else '0cm',
|
|
level=token.level,
|
|
content=self.render_inner(token)
|
|
)
|
|
|
|
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=\dimexpr1cm+<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-<colx>\tabucolX-<colnd>\tabcolsep\relax}',
|
|
colx=sum(col.weight for col in token.children[1:]),
|
|
colnd=2 * (len(token.children) - 1)
|
|
)
|
|
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]{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
|
|
)
|