pandoc_numbering/_main.py
#!/usr/bin/env python
# pylint: disable=too-many-lines
"""Pandoc filter to number all kinds of things."""
import copy
import re
import unicodedata
from functools import partial
from textwrap import dedent
from panflute import (
BlockQuote,
BulletList,
Citation,
Cite,
CodeBlock,
Definition,
DefinitionItem,
DefinitionList,
Div,
Emph,
Header,
HorizontalRule,
Image,
LineBlock,
LineBreak,
LineItem,
Link,
ListItem,
MetaBool,
MetaInlines,
MetaList,
MetaMap,
MetaString,
Note,
Para,
Plain,
RawBlock,
RawInline,
SoftBreak,
Space,
Span,
Str,
Strong,
Table,
TableCell,
TableRow,
convert_text,
debug,
run_filters,
stringify,
)
# pylint: disable=bad-option-value,useless-object-inheritance
class Numbered:
"""
Numbered elements.
"""
# pylint: disable=too-many-instance-attributes
__slots__ = [
"_elem",
"_doc",
"_match",
"_tag",
"_entry",
"_link",
"_caption",
"_title",
"_description",
"_category",
"_basic_category",
"_first_section_level",
"_last_section_level",
"_leading",
"_number",
"_global_number",
"_section_number",
"_local_number",
"_section_alias",
"_alias",
]
@property
def tag(self):
"""
Get the tag property.
Returns
-------
The tag property.
"""
return self._tag
@property
def entry(self):
"""
Get the entry property.
Returns
-------
The entry property.
"""
return self._entry
@property
def link(self):
"""
Get the link property.
Returns
-------
The link property.
"""
return self._link
@property
def title(self):
"""
Get the title property.
Returns
-------
The title property.
"""
return self._title
@property
def description(self):
"""
Get the description property.
Returns
-------
The description property.
"""
return self._description
@property
def global_number(self):
"""
Get the global_number property.
Returns
-------
The global_number property.
"""
return self._global_number
@property
def section_number(self):
"""
Get the section_number property.
Returns
-------
The section_number property.
"""
return self._section_number
@property
def section_alias(self):
"""
Get the section_alias property.
Returns
-------
The section_alias property.
"""
return self._section_alias
@property
def alias(self):
"""
Get the alias property.
Returns
-------
The alias property.
"""
return self._alias
@property
def local_number(self):
"""
Get the local_number property.
Returns
-------
The local_number property.
"""
return self._local_number
@property
def category(self):
"""
Get the category property.
Returns
-------
The category property.
"""
return self._category
@property
def caption(self):
"""
Get the caption property.
Returns
-------
The caption property.
"""
return self._caption
number_regex = "#((?P<prefix>[a-zA-Z][\\w.-]*):)?(?P<name>[a-zA-Z][\\w:.-]*)?"
_regex = "(?P<header>(?P<hidden>(-\\.)*)(\\+\\.)*)"
header_regex = "^" + _regex + "$"
marker_regex = "^" + _regex + number_regex + "$"
double_sharp_regex = "^" + _regex + "#" + number_regex + "$"
@staticmethod
def _remove_accents(string):
nfkd_form = unicodedata.normalize("NFKD", string)
# pylint: disable=redundant-u-string-prefix
return "".join([c for c in nfkd_form if not unicodedata.combining(c)])
@staticmethod
def _identifier(string):
# replace invalid characters by dash
string = re.sub(
"[^0-9a-zA-Z_-]+", "-", Numbered._remove_accents(string.lower())
)
# Remove leading digits
return re.sub("^[^a-zA-Z]+", "", string)
def __init__(self, elem, doc):
"""
Initialise an instance.
Arguments
---------
elem
An element.
doc
The document.
"""
self._elem = elem
self._doc = doc
self._tag = None
self._entry = Span(classes=["pandoc-numbering-entry"])
self._link = Span(classes=["pandoc-numbering-link"])
self._caption = None
self._title = None
self._description = None
self._category = None
self._basic_category = None
self._first_section_level = None
self._last_section_level = None
self._leading = None
self._number = None
self._global_number = None
self._section_number = None
self._local_number = None
self._section_alias = None
self._alias = None
if self._get_content() and isinstance(self._get_content()[-1], Str):
self._match = re.match(Numbered.marker_regex, self._get_content()[-1].text)
if self._match:
self._replace_marker()
elif re.match(Numbered.double_sharp_regex, self._get_content()[-1].text):
self._replace_double_sharp()
def _set_content(self, content):
if isinstance(self._elem, Para):
self._elem.content = content
elif isinstance(self._elem, DefinitionItem):
self._elem.term = content
def _get_content(self):
if isinstance(self._elem, Para):
return self._elem.content
if isinstance(self._elem, DefinitionItem):
return self._elem.term
return None
def _replace_double_sharp(self):
self._get_content()[-1].text = self._get_content()[-1].text.replace(
"##", "#", 1
)
def _replace_marker(self):
self._compute_title()
self._compute_description()
self._compute_basic_category()
self._compute_levels()
self._compute_section_number()
self._compute_section_alias()
self._compute_leading()
self._compute_category()
self._compute_number()
self._compute_tag()
self._compute_alias()
self._compute_local_number()
self._compute_global_number()
self._compute_data()
def _compute_title(self):
self._title = []
if (
isinstance(self._get_content()[-3], Str)
and self._get_content()[-3].text[-1:] == ")"
):
for i, item in enumerate(self._get_content()):
if isinstance(item, Str) and item.text[0] == "(":
self._title = self._get_content()[i:-2]
# Detach from original parent
self._title.parent = None
self._title[0].text = self._title[0].text[1:]
self._title[-1].text = self._title[-1].text[:-1]
del self._get_content()[i - 1 : -2]
break
self._title = list(self._title)
def _compute_description(self):
self._description = self._get_content()[:-2]
# Detach from original parent
self._description.parent = None
self._description = list(self._description)
def _compute_basic_category(self):
if self._match.group("prefix") is None:
self._basic_category = Numbered._identifier(
"".join(map(stringify, self._description))
)
else:
self._basic_category = self._match.group("prefix")
if self._basic_category not in self._doc.defined:
define(self._basic_category, self._doc)
def _compute_levels(self):
# Compute the first and last section level values
self._first_section_level = len(self._match.group("hidden")) // 2
self._last_section_level = len(self._match.group("header")) // 2
# Get the default first and last section level
if self._first_section_level == self._last_section_level == 0:
self._first_section_level = self._doc.defined[self._basic_category][
"first-section-level"
]
self._last_section_level = self._doc.defined[self._basic_category][
"last-section-level"
]
def _compute_section_number(self):
self._section_number = ".".join(
map(str, self._doc.headers[: self._last_section_level])
)
def _compute_section_alias(self):
strings = list(map(str, self._doc.aliases[: self._last_section_level]))
for index, string in enumerate(strings):
if string == "":
strings[index] = "0"
self._section_alias = ".".join(strings)
def _compute_leading(self):
# Compute the leading (composed of the section numbering and a dot)
if self._last_section_level != 0:
self._leading = self._section_number + "."
else:
self._leading = ""
def _compute_category(self):
self._category = self._basic_category + ":" + self._leading
# Is it a new category?
if self._category not in self._doc.count:
self._doc.count[self._category] = 0
self._doc.count[self._category] = self._doc.count[self._category] + 1
def _compute_number(self):
self._number = str(self._doc.count[self._category])
def _compute_tag(self):
# Determine the final tag
if self._match.group("name") is None:
self._tag = self._category + self._number
else:
self._tag = self._basic_category + ":" + self._match.group("name")
# Compute collections
if self._basic_category not in self._doc.collections:
self._doc.collections[self._basic_category] = []
self._doc.collections[self._basic_category].append(self._tag)
def _compute_alias(self):
# Determine the final alias
if not self._title:
if self._section_alias:
self._alias = (
self._basic_category
+ ":"
+ self._section_alias
+ "."
+ self._number
)
else:
self._alias = self._basic_category + ":" + self._number
else:
if self._section_alias:
self._alias = (
self._basic_category
+ ":"
+ self._section_alias
+ "."
+ Numbered._identifier(stringify(Span(*self._title)))
)
else:
self._alias = (
self._basic_category
+ ":"
+ Numbered._identifier(stringify(Span(*self._title)))
)
def _compute_local_number(self):
# Replace the '-.-.+.+...#' by the category count (omitting the hidden part)
self._local_number = ".".join(
map(
str,
self._doc.headers[self._first_section_level : self._last_section_level]
+ [self._number],
)
)
def _compute_global_number(self):
# Compute the global number
if self._section_number:
self._global_number = self._section_number + "." + self._number
else:
self._global_number = self._number
def _compute_data(self):
# pylint: disable=too-many-statements,no-member
classes = self._doc.defined[self._basic_category]["classes"]
if self._alias == self._tag:
self._set_content(
[
Span(),
Span(
identifier=self._tag,
classes=["pandoc-numbering-text"] + classes,
),
]
)
else:
self._set_content(
[
Span(identifier=self._alias),
Span(
identifier=self._tag,
classes=["pandoc-numbering-text"] + classes,
),
]
)
self._link.classes = self._link.classes + classes
self._entry.classes = self._entry.classes + classes
# Prepare the final data
if self._title:
self._get_content()[1].content = copy.deepcopy(
self._doc.defined[self._basic_category]["format-text-title"]
)
self._link.content = copy.deepcopy(
self._doc.defined[self._basic_category]["format-link-title"]
)
self._entry.content = copy.deepcopy(
self._doc.defined[self._basic_category]["format-entry-title"]
)
self._caption = self._doc.defined[self._basic_category][
"format-caption-title"
]
else:
self._get_content()[1].content = copy.deepcopy(
self._doc.defined[self._basic_category]["format-text-classic"]
)
self._link.content = copy.deepcopy(
self._doc.defined[self._basic_category]["format-link-classic"]
)
self._entry.content = copy.deepcopy(
self._doc.defined[self._basic_category]["format-entry-classic"]
)
self._caption = self._doc.defined[self._basic_category][
"format-caption-classic"
]
# Compute caption (delay replacing %c at the end)
title = stringify(Span(*self._title))
description = stringify(Span(*self._description))
self._caption = self._caption.replace("%t", title.lower())
self._caption = self._caption.replace("%T", title)
self._caption = self._caption.replace("%d", description.lower())
self._caption = self._caption.replace("%D", description)
self._caption = self._caption.replace("%s", self._section_number)
self._caption = self._caption.replace("%g", self._global_number)
self._caption = self._caption.replace("%n", self._local_number)
self._caption = self._caption.replace("#", self._local_number)
if self._doc.format in {"tex", "latex"}:
self._caption = self._caption.replace("%p", "\\pageref{" + self._tag + "}")
# Compute content
if isinstance(self._elem, DefinitionItem):
replace_description(Plain(*self._elem.term), self._description)
replace_title(Plain(*self._elem.term), self._title)
replace_global_number(Plain(*self._elem.term), self._global_number)
replace_section_number(Plain(*self._elem.term), self._section_number)
replace_local_number(Plain(*self._elem.term), self._local_number)
else:
replace_description(self._elem, self._description)
replace_title(self._elem, self._title)
replace_global_number(self._elem, self._global_number)
replace_section_number(self._elem, self._section_number)
replace_local_number(self._elem, self._local_number)
# Compute link
replace_description(self._link, self._description)
replace_title(self._link, self._title)
replace_global_number(self._link, self._global_number)
replace_section_number(self._link, self._section_number)
replace_local_number(self._link, self._local_number)
if self._doc.format in {"tex", "latex"}:
replace_page_number(self._link, self._tag)
# Compute entry
replace_description(self._entry, self._description)
replace_title(self._entry, self._title)
replace_global_number(self._entry, self._global_number)
replace_section_number(self._entry, self._section_number)
replace_local_number(self._entry, self._local_number)
# Finalize the content
if self._doc.format in {"tex", "latex"}:
latex_category = re.sub("[^a-z]+", "", self._basic_category)
latex = (
"\\phantomsection"
f"\\addcontentsline{{{latex_category}}}{{{latex_category}}}"
f"{{\\protect\\numberline {{{self._leading + self._number}}}"
f"{{\\ignorespaces {to_latex(self._entry)}"
"}}"
)
self._get_content().insert(0, RawInline(latex, "tex"))
def replace_description(where, description):
"""
Replace description in where.
Arguments
---------
where
where to replace
description
replace %D and %d by description
"""
where.walk(partial(replacing, search="%D", replace=copy.deepcopy(description)))
where.walk(
partial(
replacing,
search="%d",
replace=[item.walk(lowering) for item in copy.deepcopy(description)],
)
)
def replace_title(where, title):
"""
Replace title in where.
Arguments
---------
where
where to replace
title
replace %T and %t by title
"""
where.walk(partial(replacing, search="%T", replace=copy.deepcopy(title)))
where.walk(
partial(
replacing,
search="%t",
replace=[item.walk(lowering) for item in copy.deepcopy(title)],
)
)
def replace_section_number(where, section_number):
"""
Replace section number in where.
Arguments
---------
where
where to replace
section_number
replace %s by section_number
"""
where.walk(partial(replacing, search="%s", replace=[Str(section_number)]))
def replace_global_number(where, global_number):
"""
Replace global number in where.
Arguments
---------
where
where to replace
global_number
replace %g by global_number
"""
where.walk(partial(replacing, search="%g", replace=[Str(global_number)]))
def replace_local_number(where, local_number):
"""
Replace local number in where.
Arguments
---------
where
where to replace
local_number
replace %n and # by local_number
"""
where.walk(partial(replacing, search="%n", replace=[Str(local_number)]))
where.walk(partial(replacing, search="#", replace=[Str(local_number)]))
def replace_page_number(where, tag):
"""
Replace page number in where.
Arguments
---------
where
where to replace
tag
replace %p by tag
"""
where.walk(
partial(
replacing, search="%p", replace=[RawInline("\\pageref{" + tag + "}", "tex")]
)
)
def replace_count(where, count):
"""
Replace count in where.
Arguments
---------
where
where to replace
count
replace %c by count
"""
where.walk(partial(replacing, search="%c", replace=[Str(count)]))
def remove_useless_latex(elem, _):
"""
Clean up LaTeX element for entries.
Arguments
---------
elem
elem to scan
Returns
-------
[]: if elem is an instance to remove
None: otherwise
"""
if isinstance(
elem,
(
BlockQuote,
BulletList,
Citation,
Cite,
CodeBlock,
Definition,
DefinitionItem,
DefinitionList,
Div,
Header,
HorizontalRule,
Image,
LineBlock,
LineBreak,
LineItem,
ListItem,
Note,
Para,
RawBlock,
RawInline,
SoftBreak,
Table,
TableCell,
TableRow,
),
):
return []
return None
def to_latex(elem):
"""
Convert element to LaTeX.
Arguments
---------
elem
elem to convert
Returns
-------
LaTex string
"""
return convert_text(
run_filters([remove_useless_latex], doc=Plain(elem)),
input_format="panflute",
output_format="latex",
extra_args=["--no-highlight"],
)
def define(category, doc):
"""
Define a category in document.
Arguments
---------
category
category to define
doc
pandoc document
"""
# pylint: disable=line-too-long
doc.defined[category] = {
"first-section-level": 0,
"last-section-level": 0,
"format-text-classic": [Strong(Str("%D"), Space(), Str("%n"))],
"format-text-title": [
Strong(Str("%D"), Space(), Str("%n")),
Space(),
Emph(Str("(%T)")),
],
"format-link-classic": [Str("%D"), Space(), Str("%n")],
"format-link-title": [Str("%D"), Space(), Str("%n"), Space(), Str("(%T)")],
"format-caption-classic": "%D %n",
"format-caption-title": "%D %n (%T)",
"format-entry-title": [Str("%T")],
"classes": [category],
"cite-shortcut": True,
"listing-title": None,
"listing-unnumbered": True,
"listing-unlisted": True,
"listing-identifier": True,
"entry-tab": 1.5,
"entry-space": 2.3,
}
if doc.format == "latex":
doc.defined[category]["format-entry-classic"] = [Str("%D")]
doc.defined[category]["entry-tab"] = 1.5
doc.defined[category]["entry-space"] = 2.3
else:
doc.defined[category]["format-entry-classic"] = [Str("%D"), Space(), Str("%g")]
def lowering(elem, _):
"""
Lower element.
Arguments
---------
elem
element to lower
"""
if isinstance(elem, Str):
elem.text = elem.text.lower()
def replacing(elem, _, search=None, replace=None):
"""
Replace an element.
Arguments
---------
elem
element to scan
search
string to search
replace
string to replace
Returns
-------
The modified elements.
"""
if isinstance(elem, Str):
search_splitted = elem.text.split(search)
if len(search_splitted) > 1:
text = []
if search_splitted[0] != "":
text.append(Str(search_splitted[0]))
for string in search_splitted[1:]:
text.extend(replace)
if string != "":
text.append(Str(string))
return text
return [elem]
def numbering(elem, doc):
"""
Add the numbering of an element.
Arguments
---------
elem
element to number
doc
pandoc document
"""
if isinstance(elem, Header):
update_header_numbers(elem, doc)
update_header_aliases(elem, doc)
elif isinstance(elem, (Para, DefinitionItem)):
numbered = Numbered(elem, doc)
if numbered.tag is not None:
doc.information[numbered.tag] = numbered
def referencing(elem, doc):
"""
Add a reference for an element.
Arguments
---------
elem
element to reference
doc
pandoc document
Returns
-------
A Link or None
"""
if isinstance(elem, Link):
return referencing_link(elem, doc)
if isinstance(elem, Cite):
return referencing_cite(elem, doc)
if isinstance(elem, Span) and elem.identifier in doc.information:
replace_count(elem, str(doc.count[doc.information[elem.identifier].category]))
return None
def referencing_link(elem, doc):
"""
Add a eference link.
Arguments
---------
elem
element to reference
doc
pandoc document
"""
match = re.match("^#(?P<tag>([a-zA-Z][\\w:.-]*))$", elem.url)
if match:
tag = match.group("tag")
if tag in doc.information:
replace_title(elem, doc.information[tag].title)
replace_description(elem, doc.information[tag].description)
replace_global_number(elem, doc.information[tag].global_number)
replace_section_number(elem, doc.information[tag].section_number)
replace_local_number(elem, doc.information[tag].local_number)
replace_count(elem, str(doc.count[doc.information[tag].category]))
if doc.format in {"tex", "latex"}:
replace_page_number(elem, tag)
title = stringify(Span(*doc.information[tag].title))
description = stringify(Span(*doc.information[tag].description))
elem.title = elem.title.replace("%t", title.lower())
elem.title = elem.title.replace("%T", title)
elem.title = elem.title.replace("%d", description.lower())
elem.title = elem.title.replace("%D", description)
elem.title = elem.title.replace("%s", doc.information[tag].section_number)
elem.title = elem.title.replace("%g", doc.information[tag].global_number)
elem.title = elem.title.replace("%n", doc.information[tag].local_number)
elem.title = elem.title.replace("#", doc.information[tag].local_number)
elem.title = elem.title.replace(
"%c", str(doc.count[doc.information[tag].category])
)
if doc.format in {"tex", "latex"}:
elem.title = elem.title.replace("%p", "\\pageref{" + tag + "}")
def referencing_cite(elem, doc):
"""
Cite reference.
Arguments
---------
elem
element to reference
doc
pandoc document
Returns
-------
A Link or None
"""
if len(elem.content) == 1 and isinstance(elem.content[0], Str):
match = re.match(
"^(@(?P<tag>(?P<category>[a-zA-Z][\\w.-]*):"
"(([a-zA-Z][\\w.-]*)|(\\d*(\\.\\d*)*))))$",
elem.content[0].text,
)
if match:
category = match.group("category")
if category in doc.defined and doc.defined[category]["cite-shortcut"]:
# Deal with @prefix:name shortcut
tag = match.group("tag")
if tag in doc.information:
ret = Link(
doc.information[tag].link,
url="#" + tag,
title=doc.information[tag].caption.replace(
"%c", str(doc.count[doc.information[tag].category])
),
)
replace_count(ret, str(doc.count[doc.information[tag].category]))
return ret
return None
def update_header_numbers(elem, doc):
"""
Update header numbers.
Arguments
---------
elem
element to update
doc
pandoc document
"""
if "unnumbered" not in elem.classes:
doc.headers[elem.level - 1] = doc.headers[elem.level - 1] + 1
for index in range(elem.level, 6):
doc.headers[index] = 0
def update_header_aliases(elem, doc):
"""
Update header aliases.
Arguments
---------
elem
element to update
doc
pandoc document
"""
doc.aliases[elem.level - 1] = elem.identifier
for index in range(elem.level, 6):
doc.aliases[index] = ""
def prepare(doc):
"""
Prepare document.
Arguments
---------
doc
pandoc document
"""
doc.headers = [0, 0, 0, 0, 0, 0]
doc.aliases = ["", "", "", "", "", ""]
doc.information = {}
doc.defined = {}
if "pandoc-numbering" in doc.metadata.content and isinstance(
doc.metadata.content["pandoc-numbering"], MetaMap
):
for category, definition in doc.metadata.content[
"pandoc-numbering"
].content.items():
if isinstance(definition, MetaMap):
add_definition(category, definition, doc)
doc.count = {}
doc.collections = {}
def add_definition(category, definition, doc):
"""
Add definition for a category.
Arguments
---------
category
The category
definition
The definition
doc
The pandoc document
"""
# Create the category with options by default
define(category, doc)
# Detect general options
if "general" in definition:
meta_cite(category, definition["general"], doc.defined)
meta_listing(category, definition["general"], doc.defined)
meta_levels(category, definition["general"], doc.defined)
meta_classes(category, definition["general"], doc.defined)
# Detect LaTeX options
if doc.format in {"tex", "latex"}:
if "latex" in definition:
meta_format_text(category, definition["latex"], doc.defined)
meta_format_link(category, definition["latex"], doc.defined)
meta_format_caption(category, definition["latex"], doc.defined)
meta_format_entry(category, definition["latex"], doc.defined)
meta_entry_tab(category, definition["latex"], doc.defined)
meta_entry_space(category, definition["latex"], doc.defined)
# Detect standard options
else:
if "standard" in definition:
meta_format_text(category, definition["standard"], doc.defined)
meta_format_link(category, definition["standard"], doc.defined)
meta_format_caption(category, definition["standard"], doc.defined)
meta_format_entry(category, definition["standard"], doc.defined)
def meta_cite(category, definition, defined):
"""
Compute cite for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
if "cite-shortcut" in definition:
if isinstance(definition["cite-shortcut"], MetaBool):
defined[category]["cite-shortcut"] = definition["cite-shortcut"].boolean
else:
debug(
"[WARNING] pandoc-numbering: cite-shortcut is not correct for category "
+ category
)
def meta_format(category, definition, defined, tag):
"""
Compute format text for a category and a tag.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
tag
The tag parameter
"""
if tag in definition:
if isinstance(definition[tag], MetaInlines):
# Detach from original parent
defined[category][tag] = definition[tag].content
defined[category][tag].parent = None
else:
debug(
f"[WARNING] pandoc-numbering: "
f"{tag} is not correct for category {category}"
)
# pylint:disable=too-many-branches
def meta_listing(category, definition, defined):
"""
Compute listing for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
meta_format(category, definition, defined, "listing-title")
for key in ("listing-unnumbered", "listing-unlisted"):
if key in definition:
if isinstance(definition[key], MetaBool):
defined[category][key] = definition[key].boolean
else:
debug(
f"[WARNING] pandoc-numbering: "
f"{key} is not correct for category {category}"
)
if "listing-identifier" in definition:
if isinstance(definition["listing-identifier"], MetaBool):
defined[category]["listing-identifier"] = definition[
"listing-identifier"
].boolean
elif (
isinstance(definition["listing-identifier"], MetaInlines)
and len(definition["listing-identifier"].content) == 1
and isinstance(definition["listing-identifier"].content[0], Str)
):
defined[category]["listing-identifier"] = (
definition["listing-identifier"].content[0].text
)
else:
debug(
"[WARNING] pandoc-numbering: "
"listing-identifier is not correct for category " + category
)
def meta_format_text(category, definition, defined):
"""
Compute format text for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
meta_format(category, definition, defined, "format-text-classic")
meta_format(category, definition, defined, "format-text-title")
def meta_format_link(category, definition, defined):
"""
Compute format link for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
meta_format(category, definition, defined, "format-link-classic")
meta_format(category, definition, defined, "format-link-title")
def meta_format_entry(category, definition, defined):
"""
Compute format entry for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
meta_format(category, definition, defined, "format-entry-classic")
meta_format(category, definition, defined, "format-entry-title")
def meta_format_caption(category, definition, defined):
"""
Compute format caption for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
for tag in ("format-caption-classic", "format-caption-title"):
if tag in definition:
if isinstance(definition[tag], MetaInlines):
defined[category][tag] = stringify(definition[tag])
else:
debug(
f"[WARNING] pandoc-numbering: "
f"{tag} is not correct for category {category}"
)
def meta_entry(category, definition, defined, tag):
"""
Compute entry tab for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
tag
The tag parameter
"""
if tag in definition:
if isinstance(definition[tag], MetaString):
value = definition[tag].text
elif (
isinstance(definition[tag], MetaInlines)
and len(definition[tag].content) == 1
):
value = definition[tag].content[0].text
else:
debug(
f"[WARNING] pandoc-numbering: "
f"{tag} is not correct for category {category}"
)
return
# Get the element
try:
element = float(value)
if element > 0:
defined[category][tag] = element
else:
debug(
f"[WARNING] pandoc-numbering: "
f"{tag} must be positive for category {category}"
)
except ValueError:
debug(
f"[WARNING] pandoc-numbering: "
f"{tag} is not correct for category {category}"
)
def meta_entry_tab(category, definition, defined):
"""
Compute entry tab for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
meta_entry(category, definition, defined, "entry-tab")
def meta_entry_space(category, definition, defined):
"""
Compute entry space for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
meta_entry(category, definition, defined, "entry-space")
def meta_levels(category, definition, defined):
"""
Compute level for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
if (
"sectioning-levels" in definition
and isinstance(definition["sectioning-levels"], MetaInlines)
and len(definition["sectioning-levels"].content) == 1
):
match = re.match(
Numbered.header_regex, definition["sectioning-levels"].content[0].text
)
if match:
# Compute the first and last levels section
defined[category]["first-section-level"] = len(match.group("hidden")) // 2
defined[category]["last-section-level"] = len(match.group("header")) // 2
if "first-section-level" in definition:
if isinstance(definition["first-section-level"], MetaString):
value = definition["first-section-level"].text
elif (
isinstance(definition["first-section-level"], MetaInlines)
and len(definition["first-section-level"].content) == 1
):
value = definition["first-section-level"].content[0].text
else:
debug(
"[WARNING] pandoc-numbering: "
"first-section-level is not correct for category " + category
)
return
# Get the level
try:
level = int(value) - 1
except ValueError:
debug(
"[WARNING] pandoc-numbering: "
"first-section-level is not correct for category " + category
)
if 0 <= level <= 6:
defined[category]["first-section-level"] = level
else:
# pylint: disable=line-too-long
debug(
"[WARNING] pandoc-numbering: "
"first-section-level must be positive or zero for category " + category
)
if "last-section-level" in definition:
if isinstance(definition["last-section-level"], MetaString):
value = definition["last-section-level"].text
elif (
isinstance(definition["last-section-level"], MetaInlines)
and len(definition["last-section-level"].content) == 1
):
value = definition["last-section-level"].content[0].text
else:
debug(
"[WARNING] pandoc-numbering: "
"last-section-level is not correct for category " + category
)
return
# Get the level
try:
level = int(value)
except ValueError:
debug(
"[WARNING] pandoc-numbering: "
"last-section-level is not correct for category " + category
)
if 0 <= level <= 6:
defined[category]["last-section-level"] = level
else:
# pylint: disable=line-too-long
debug(
"[WARNING] pandoc-numbering: "
"last-section-level must be positive or zero for category " + category
)
def meta_classes(category, definition, defined):
"""
Compute classes for a category.
Arguments
---------
category
The category
definition
The definition
defined
The defined parameter
"""
if "classes" in definition and isinstance(definition["classes"], MetaList):
defined[category]["classes"] = [
stringify(elt) for elt in definition["classes"].content
]
def finalize(doc):
"""
Finalize document.
Arguments
---------
doc
The pandoc document
"""
# Loop on all listings definition
if doc.format in {"tex", "latex"}:
# Add header-includes if necessary
if "header-includes" not in doc.metadata:
doc.metadata["header-includes"] = MetaList()
# Convert header-includes to MetaList if necessary
elif not isinstance(doc.metadata["header-includes"], MetaList):
doc.metadata["header-includes"] = MetaList(doc.metadata["header-includes"])
doc.metadata["header-includes"].append(
MetaInlines(
RawInline(
dedent(
r"""
\makeatletter
\@ifpackageloaded{subfig}{
\usepackage[subfigure]{tocloft}
}{
\usepackage{tocloft}
}
\makeatother
"""
),
"tex",
)
)
)
doc.metadata["header-includes"].append(
MetaInlines(RawInline(r"\usepackage{etoolbox}", "tex"))
)
i = 0
listof = []
for category, definition in doc.defined.items():
if definition["listing-title"] is not None:
# pylint: disable=consider-using-f-string
if doc.format in {"tex", "latex"}:
latex_category = re.sub("[^a-z]+", "", category)
latex = (
r"\newlistof{%s}{%s}{%s}"
r"\renewcommand{\cft%stitlefont}{\cfttoctitlefont}"
r"\setlength{\cft%snumwidth}{\cftfignumwidth}"
r"\setlength{\cft%sindent}{\cftfigindent}"
% (
latex_category,
latex_category,
convert_text(
Plain(*definition["listing-title"]),
input_format="panflute",
output_format="latex",
),
latex_category,
latex_category,
latex_category,
)
)
doc.metadata["header-includes"].append(
MetaInlines(RawInline(latex, "tex"))
)
listof.append(f"\\listof{latex_category}")
else:
classes = ["pandoc-numbering-listing"] + definition["classes"]
if definition["listing-unnumbered"]:
classes.append("unnumbered")
if definition["listing-unlisted"]:
classes.append("unlisted")
if definition["listing-identifier"] is False:
header = Header(
*definition["listing-title"], level=1, classes=classes
)
elif definition["listing-identifier"] is True:
header = Header(
*definition["listing-title"], level=1, classes=classes
)
header = convert_text(
convert_text(
header, input_format="panflute", output_format="markdown"
),
output_format="panflute",
)[0]
else:
header = Header(
*definition["listing-title"],
level=1,
classes=classes,
identifier=definition["listing-identifier"],
)
doc.content.insert(i, header)
i = i + 1
table = table_other(doc, category, definition)
if table:
doc.content.insert(i, table)
i = i + 1
if doc.format in {"tex", "latex"}:
header = (
r"\ifdef{\mainmatter}"
r"{\let\oldmainmatter\mainmatter"
r"\renewcommand{\mainmatter}[0]{%s\oldmainmatter}}"
r"{}"
)
doc.metadata["header-includes"].append(
MetaInlines(RawInline(header % "\n".join(listof), "tex"))
)
latex = r"\ifdef{\mainmatter}{}{%s}"
doc.content.insert(0, RawBlock(latex % "\n".join(listof), "tex"))
def table_other(doc, category, _):
"""
Compute other code for table.
Arguments
---------
doc
pandoc document
category
category numbered
Returns
-------
A BulletList or None
"""
if category in doc.collections:
# Return a bullet list
return BulletList(
*(
ListItem(Plain(Link(doc.information[tag].entry, url="#" + tag)))
for tag in doc.collections[category]
)
)
return None
def link_color(doc):
"""
Compute LaTeX code for toc.
Arguments
---------
doc
pandoc document
Returns
-------
LaTeX code for links.
"""
# Get the link color
metadata = doc.get_metadata()
if "toccolor" in metadata:
return "\\hypersetup{linkcolor=" + str(metadata["toccolor"]) + "}"
return "\\hypersetup{linkcolor=black}"
def main(doc=None) -> None:
"""
Produce the final document.
Parameters
----------
doc
pandoc document
"""
run_filters([numbering, referencing], prepare=prepare, doc=doc, finalize=finalize)