pubmarq 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pubmarq/__init__.py +45 -0
- pubmarq/__main__.py +6 -0
- pubmarq/article.py +600 -0
- pubmarq/build.py +212 -0
- pubmarq/check.py +221 -0
- pubmarq/cli.py +164 -0
- pubmarq/emblems/umg-pl.svg +53 -0
- pubmarq/faces/CascadiaCode/CascadiaCode-Bold.ttf +0 -0
- pubmarq/faces/CascadiaCode/CascadiaCode-BoldItalic.ttf +0 -0
- pubmarq/faces/CascadiaCode/CascadiaCode-Italic.ttf +0 -0
- pubmarq/faces/CascadiaCode/CascadiaCode-Regular.ttf +0 -0
- pubmarq/faces/IBMPlexMono/IBMPlexMono-Bold.ttf +0 -0
- pubmarq/faces/IBMPlexMono/IBMPlexMono-BoldItalic.ttf +0 -0
- pubmarq/faces/IBMPlexMono/IBMPlexMono-Italic.ttf +0 -0
- pubmarq/faces/IBMPlexMono/IBMPlexMono-Regular.ttf +0 -0
- pubmarq/faces/OFL-CascadiaCode.txt +88 -0
- pubmarq/faces/OFL-IBMPlex.txt +88 -0
- pubmarq/faces/OFL-OpenSans.txt +88 -0
- pubmarq/faces/OFL-Sora.txt +88 -0
- pubmarq/faces/OFL-SourceSerif4.txt +88 -0
- pubmarq/faces/OFL-Tinos.txt +88 -0
- pubmarq/faces/OpenSans/OpenSans-Bold.ttf +0 -0
- pubmarq/faces/OpenSans/OpenSans-BoldItalic.ttf +0 -0
- pubmarq/faces/OpenSans/OpenSans-Italic.ttf +0 -0
- pubmarq/faces/OpenSans/OpenSans-Regular.ttf +0 -0
- pubmarq/faces/OpenSansCondensed/OpenSansCondensed-Bold.ttf +0 -0
- pubmarq/faces/OpenSansCondensed/OpenSansCondensed-BoldItalic.ttf +0 -0
- pubmarq/faces/OpenSansCondensed/OpenSansCondensed-Italic.ttf +0 -0
- pubmarq/faces/OpenSansCondensed/OpenSansCondensed-Regular.ttf +0 -0
- pubmarq/faces/OpenSansSemiBold/OpenSansSemiBold-Bold.ttf +0 -0
- pubmarq/faces/OpenSansSemiBold/OpenSansSemiBold-BoldItalic.ttf +0 -0
- pubmarq/faces/OpenSansSemiBold/OpenSansSemiBold-Italic.ttf +0 -0
- pubmarq/faces/OpenSansSemiBold/OpenSansSemiBold-Regular.ttf +0 -0
- pubmarq/faces/Sora/Sora-Bold.ttf +0 -0
- pubmarq/faces/Sora/Sora-Regular.ttf +0 -0
- pubmarq/faces/SourceSerif4/SourceSerif4-Bold.ttf +0 -0
- pubmarq/faces/SourceSerif4/SourceSerif4-BoldItalic.ttf +0 -0
- pubmarq/faces/SourceSerif4/SourceSerif4-Italic.ttf +0 -0
- pubmarq/faces/SourceSerif4/SourceSerif4-Regular.ttf +0 -0
- pubmarq/faces/Tinos/Tinos-Bold.ttf +0 -0
- pubmarq/faces/Tinos/Tinos-BoldItalic.ttf +0 -0
- pubmarq/faces/Tinos/Tinos-Italic.ttf +0 -0
- pubmarq/faces/Tinos/Tinos-Regular.ttf +0 -0
- pubmarq/fonts.py +79 -0
- pubmarq/journal.py +348 -0
- pubmarq/journals.py +285 -0
- pubmarq/langs.py +82 -0
- pubmarq/layout.py +423 -0
- pubmarq/pages.py +30 -0
- pubmarq/refs.py +149 -0
- pubmarq/thesis_docx.py +414 -0
- pubmarq/thesis_pdf.py +422 -0
- pubmarq/to_docx.py +427 -0
- pubmarq/to_pdf.py +858 -0
- pubmarq/typography.py +69 -0
- pubmarq-0.1.0.dist-info/METADATA +153 -0
- pubmarq-0.1.0.dist-info/RECORD +61 -0
- pubmarq-0.1.0.dist-info/WHEEL +5 -0
- pubmarq-0.1.0.dist-info/entry_points.txt +2 -0
- pubmarq-0.1.0.dist-info/licenses/license +21 -0
- pubmarq-0.1.0.dist-info/top_level.txt +1 -0
pubmarq/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# pubmarq/__init__.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Scientific articles and theses from markdown, set for a journal or a school:
|
|
5
|
+
PDF through pdfmarq, DOCX through docmarq.
|
|
6
|
+
|
|
7
|
+
The article says what it is, a journal profile how it looks,
|
|
8
|
+
so one source goes to any journal pubmarq has a profile for.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from pubmarq import build
|
|
12
|
+
>>> build("paper.md", "ijet", out_dir="out").files
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
#--------------------------------------------------------------------------- Metadata for auto-toml
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
__repo__ = "Xaeian/PubMarQ"
|
|
19
|
+
__python__ = ">=3.12" # as xaeian
|
|
20
|
+
__description__ = "Scientific articles and theses from markdown, set for a journal as PDF and DOCX"
|
|
21
|
+
__author__ = "Xaeian"
|
|
22
|
+
__keywords__ = ["journal", "article", "thesis", "markdown", "pdf", "docx", "ieee"]
|
|
23
|
+
__dependencies__ = [
|
|
24
|
+
"docmarq[md]>=0.5.1", "pdfmarq[md]>=0.7.1", "markdown-it-py", "Pillow", "xaeian>=1.2.2",
|
|
25
|
+
"rlPyCairo", # docmarq's, named so CI installs the system cairo pycairo builds against
|
|
26
|
+
]
|
|
27
|
+
__scripts__ = {"pubmarq": "pubmarq.cli:main"}
|
|
28
|
+
|
|
29
|
+
#--------------------------------------------------------------------------------------- Public API
|
|
30
|
+
|
|
31
|
+
from .langs import Lang, LANGS
|
|
32
|
+
from .journal import Foot, Head, Journal, RunIn, School, Type
|
|
33
|
+
from .journals import JOURNALS, get_journal
|
|
34
|
+
from .article import Article, Parser, read
|
|
35
|
+
from .check import Issue, check
|
|
36
|
+
from .to_docx import write_docx
|
|
37
|
+
from .to_pdf import write_pdf
|
|
38
|
+
from .build import Result, build, preflight, read_md, read_file
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"Lang", "LANGS", "Foot", "Head", "RunIn", "School", "Type", "Journal", "JOURNALS", "get_journal",
|
|
42
|
+
"Article", "Parser", "read", "Issue", "check",
|
|
43
|
+
"write_docx", "write_pdf",
|
|
44
|
+
"Result", "build", "preflight", "read_md", "read_file",
|
|
45
|
+
]
|
pubmarq/__main__.py
ADDED
pubmarq/article.py
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# pubmarq/article.py
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
An article read from markdown: its parts, in the order and with the numbers a journal sets.
|
|
5
|
+
|
|
6
|
+
`read` takes the text and an engine's `Parser`, and resolves for one journal:
|
|
7
|
+
abstracts and keywords, heading numbers, figures and tables with their captions,
|
|
8
|
+
citations as reference numbers, mentions of figures, tables and labelled formulas, typography.
|
|
9
|
+
Writers draw the blocks, and nothing here knows Word, PDF or an engine.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> article = read(open("paper.md").read(), get_journal("ijet"), parser)
|
|
13
|
+
>>> [b.kind for b in article.blocks][:3]
|
|
14
|
+
['heading', 'para', 'para']
|
|
15
|
+
"""
|
|
16
|
+
import re
|
|
17
|
+
from dataclasses import dataclass, field, replace
|
|
18
|
+
from typing import Callable
|
|
19
|
+
from markdown_it.token import Token
|
|
20
|
+
from .journal import Journal
|
|
21
|
+
from .langs import LANGS
|
|
22
|
+
from .refs import entries, format_ref, has_doi, inline, text
|
|
23
|
+
from .typography import EN_DASH, NBSP, bind, counter, roman, smarten
|
|
24
|
+
|
|
25
|
+
# The names a source is read by, in every language: a heading or an opening, to its language
|
|
26
|
+
_ABSTRACT = {lang.abstract.lower(): key for key, lang in LANGS.items()}
|
|
27
|
+
_KEYWORD_NAMES = {name: key for key, lang in LANGS.items()
|
|
28
|
+
for name in (lang.keywords.lower(), *lang.keyword_names)}
|
|
29
|
+
_KEYWORDS = re.compile(rf"\s*({'|'.join(map(re.escape, _KEYWORD_NAMES))})\s*:?\s*(.*)",
|
|
30
|
+
re.I | re.S)
|
|
31
|
+
_TABLES = "|".join(lang.table for lang in LANGS.values())
|
|
32
|
+
_FIGURES = "|".join(lang.figure for lang in LANGS.values())
|
|
33
|
+
_NUMBER = r"\d+(?:\.\d+)*" # of a figure or table as a source writes it: `3`, or `2.1` by chapter
|
|
34
|
+
_CAPTION = re.compile(
|
|
35
|
+
rf"(?:(?P<table>{_TABLES})|(?P<figure>{_FIGURES}))\s*(?P<number>{_NUMBER})\.?", re.I)
|
|
36
|
+
# `Table 2`, `Tables 1 and 3`, `Figs. 2-4`, `Rys. 1 i 2`: the word, and the numbers after it
|
|
37
|
+
_TABLE_NAMES = "|".join(lang.table_mention for lang in LANGS.values())
|
|
38
|
+
_FIGURE_NAMES = "|".join(lang.figure_mention for lang in LANGS.values())
|
|
39
|
+
_JOINS = "|".join(word for lang in LANGS.values() for word in lang.joins)
|
|
40
|
+
_LIST = (rf"{_NUMBER}(?:(?:\s*,\s*(?:(?:{_JOINS})\s+)?|\s+(?:{_JOINS}|&)\s+|\s*[-–]\s*)"
|
|
41
|
+
rf"{_NUMBER})*")
|
|
42
|
+
_MENTION = re.compile(
|
|
43
|
+
rf"\b(?P<word>(?P<table>{_TABLE_NAMES})|(?P<figure>{_FIGURE_NAMES}))\s+(?P<list>{_LIST})\b")
|
|
44
|
+
# A formula named as LaTeX names it, `\label{ohm}` in its `$$`, and mentioned so in the text,
|
|
45
|
+
# in `$...$` or out of it: `\eqref{ohm}` its number in brackets, `\ref{ohm}` the number alone
|
|
46
|
+
_LABEL = re.compile(r"\\label\{([^{}]+)\}")
|
|
47
|
+
_EQREF = re.compile(r"\\(eq)?ref\{([^{}]+)\}")
|
|
48
|
+
# Chapters of a thesis named as lists of figures or tables, in any language, which list them
|
|
49
|
+
_LISTS = {"figure": re.compile("|".join(lang.figure_list for lang in LANGS.values()), re.I),
|
|
50
|
+
"table": re.compile("|".join(lang.table_list for lang in LANGS.values()), re.I)}
|
|
51
|
+
_RANGE = re.compile(r"\s*[-–]\s*")
|
|
52
|
+
_DROPPED = re.compile("(" + "|".join(name for lang in LANGS.values() for name in lang.refs) + ")",
|
|
53
|
+
re.I)
|
|
54
|
+
_ACK = re.compile("(" + "|".join(lang.ack for lang in LANGS.values()) + ")", re.I)
|
|
55
|
+
# `published: IJET 2027, 73(1), 245-250`: journal, year, volume(issue), pages, as written
|
|
56
|
+
_PUBLISHED = re.compile(r"(?P<journal>.+?)\s+(?P<year>\S+),\s*(?P<volume>[^\s(,]+)"
|
|
57
|
+
r"\((?P<issue>[^)]*)\),\s*(?P<pages>\S+)")
|
|
58
|
+
_ANONYMOUS = re.compile(r"Anonymous(?:(?:,? and |, )Anonymous)+") # authors of one reference
|
|
59
|
+
_BREAKS = ("softbreak", "hardbreak") # where a line of the source ends within a paragraph
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Parser:
|
|
63
|
+
"""What reading takes from an engine: its markdown, as the engine reads it."""
|
|
64
|
+
parse: Callable[[str], list[Token]] # `MarkdownRenderer.parse`
|
|
65
|
+
blocks: Callable[[list[Token]], list[tuple[int, int]]] # `top_blocks`
|
|
66
|
+
frontmatter: Callable[[str], tuple[dict, str]] # `split_frontmatter`
|
|
67
|
+
people: Callable[[object], list[dict]] # `people`
|
|
68
|
+
group_options: Callable[[str], list[str]] # `group_options`
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class Author:
|
|
72
|
+
name: str
|
|
73
|
+
aff: tuple = () # keys of `Article.affiliations`
|
|
74
|
+
email: str = ""
|
|
75
|
+
orcid: str = ""
|
|
76
|
+
corresponding: bool = False # the one to write to, `corresponding: true` in the frontmatter
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class Block:
|
|
80
|
+
"""One part a writer draws: the tokens an engine draws, and what pubmarq sets around them."""
|
|
81
|
+
# One of heading, para, figure, table, math, raw, wide, narrow, pages of a PDF set in,
|
|
82
|
+
# and abstract, keywords, authors, refs, summary where the layout's `flow` puts them
|
|
83
|
+
kind: str
|
|
84
|
+
tokens: list = field(default_factory=list)
|
|
85
|
+
text: Token|None = None # inline text of a heading or a caption
|
|
86
|
+
label: str = "" # `II.`, `Fig. 3.`, `TABLE II`
|
|
87
|
+
level: int = 0 # of a heading, from 1
|
|
88
|
+
first: bool = False # the paragraph a drop cap opens
|
|
89
|
+
ack: bool = False # part of the acknowledgment, which a blind copy leaves out
|
|
90
|
+
number: str = "" # the author's own number of a figure or table, as written: `3`, `2.1`
|
|
91
|
+
n: int = 0 # its number in reading order
|
|
92
|
+
lang: str = "" # of an abstract a thesis sets on a page of its own at the back
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class Ref:
|
|
96
|
+
number: int
|
|
97
|
+
text: Token # inline, set in the journal's style
|
|
98
|
+
doi: bool
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class Article:
|
|
102
|
+
title: str
|
|
103
|
+
short_title: str
|
|
104
|
+
authors: list
|
|
105
|
+
affiliations: dict
|
|
106
|
+
funding: str
|
|
107
|
+
abstracts: dict # language → inline tokens, one per paragraph
|
|
108
|
+
keywords: dict # language → words
|
|
109
|
+
blocks: list
|
|
110
|
+
refs: list
|
|
111
|
+
published: dict # `journal`, `year`, `volume`, `issue`, `pages` and `doi` known so far
|
|
112
|
+
meta: dict # the frontmatter as written
|
|
113
|
+
facts: dict # what reading noticed, for the preflight
|
|
114
|
+
lang: str = "en" # language it is written in, as `lang:` says, else its journal's
|
|
115
|
+
|
|
116
|
+
def title_in(self, lang:str) -> str:
|
|
117
|
+
"""The title in `lang`: `title` in its own language, else `title_<lang>` of the frontmatter."""
|
|
118
|
+
return self.title if lang == self.lang else str(self.meta.get(f"title_{lang}") or "")
|
|
119
|
+
|
|
120
|
+
def blind(self) -> "Article":
|
|
121
|
+
"""
|
|
122
|
+
The article for blind review: no authors, affiliations, funding or acknowledgment,
|
|
123
|
+
and in the references `Anonymous` for the names of its authors.
|
|
124
|
+
"""
|
|
125
|
+
names = re.compile("|".join(_name(a.name) for a in self.authors)) if self.authors else None
|
|
126
|
+
return replace(self, authors=[], affiliations={}, funding="",
|
|
127
|
+
blocks=[b for b in self.blocks if not b.ack],
|
|
128
|
+
refs=[replace(r, text=_unnamed(r.text, names)) for r in self.refs] if names else self.refs)
|
|
129
|
+
|
|
130
|
+
def plain(inline:Token, math:bool=False) -> str:
|
|
131
|
+
"""
|
|
132
|
+
Words of an inline token, code among them, markup dropped, formulas as written by `math`,
|
|
133
|
+
a space where a line of the source ends.
|
|
134
|
+
"""
|
|
135
|
+
kinds = ("text", "code_inline", "math_inline") if math else ("text", "code_inline")
|
|
136
|
+
return "".join(" " if c.type in _BREAKS else c.content for c in inline.children or []
|
|
137
|
+
if c.type in kinds + _BREAKS)
|
|
138
|
+
|
|
139
|
+
#--------------------------------------------------------------------------------------------- Read
|
|
140
|
+
|
|
141
|
+
def read(md_text:str, journal:Journal, parser:Parser) -> Article:
|
|
142
|
+
"""
|
|
143
|
+
The article `md_text` holds, resolved for `journal`, its tokens from an engine's `parser`,
|
|
144
|
+
in the language its frontmatter's `lang:` names, else the journal's.
|
|
145
|
+
"""
|
|
146
|
+
fm, _ = parser.frontmatter(md_text)
|
|
147
|
+
journal = journal.speaking(fm.get("lang"))
|
|
148
|
+
tokens = parser.parse(md_text)
|
|
149
|
+
title = str(fm.get("title") or "")
|
|
150
|
+
facts = {"uncited": [], "missing": [], "renumbered": [], "mentioned": set(), "images": [],
|
|
151
|
+
"plain": 0, "bold": 0, "labels": {}, "referred": set(), "unlabelled": [], "doubled": [],
|
|
152
|
+
"apart": set()}
|
|
153
|
+
blocks, abstracts, keywords = _structure(_items(tokens, parser), title, fm, journal)
|
|
154
|
+
refs = _cite(blocks, abstracts, _notes(tokens), journal, facts)
|
|
155
|
+
_number(blocks, journal, facts)
|
|
156
|
+
_formulas(blocks, abstracts, facts)
|
|
157
|
+
cells = {id(t) for b in blocks if b.kind == "table" for t in b.tokens if t.type == "inline"}
|
|
158
|
+
for piece in _inlines(blocks, abstracts):
|
|
159
|
+
_polish(piece, journal, facts, bold=journal.bold != "drop" or id(piece) in cells,
|
|
160
|
+
text=id(piece) not in cells)
|
|
161
|
+
if not journal.caption_stop:
|
|
162
|
+
for b in blocks:
|
|
163
|
+
if b.kind in ("figure", "table") and b.text is not None: _unstopped(b.text)
|
|
164
|
+
_run_in(blocks, journal)
|
|
165
|
+
authors, affiliations = _authors(fm, parser.people)
|
|
166
|
+
return Article(title=title, short_title=str(fm.get("short_title") or title),
|
|
167
|
+
authors=authors, affiliations=affiliations, funding=str(fm.get("funding") or "").strip(),
|
|
168
|
+
abstracts=abstracts, keywords=keywords, blocks=blocks, refs=refs,
|
|
169
|
+
published=_published(fm, facts), meta=fm, facts=facts, lang=journal.lang)
|
|
170
|
+
|
|
171
|
+
def _items(tokens:list[Token], parser:Parser) -> list[tuple[str, list[Token]]]:
|
|
172
|
+
"""
|
|
173
|
+
Top-level blocks as `(kind, tokens)`, captions paired with what they name.
|
|
174
|
+
A group is opened, and a `wide` one set between `wide` and `narrow` marks.
|
|
175
|
+
"""
|
|
176
|
+
items = []
|
|
177
|
+
for start, end in parser.blocks(tokens):
|
|
178
|
+
part, t = tokens[start:end], tokens[start]
|
|
179
|
+
if t.type == "html_block":
|
|
180
|
+
if len(part) == 1: continue # a comment, not a group `blocks` cut out
|
|
181
|
+
inner = _items(part[1:-1], parser)
|
|
182
|
+
wide = "wide" in parser.group_options(t.content)
|
|
183
|
+
items += ([("wide", [])] + inner + [("narrow", [])]) if wide else inner
|
|
184
|
+
elif t.type == "paragraph_open":
|
|
185
|
+
images = all(c.type in ("image", "softbreak") for c in part[1].children or [])
|
|
186
|
+
items.append(("image" if images else "para", part))
|
|
187
|
+
elif t.type == "blockquote_open": items.append(("caption" if _caption(part) else "raw", part))
|
|
188
|
+
elif t.type in ("heading_open", "table_open", "math_block"):
|
|
189
|
+
items.append((t.type.split("_")[0], part))
|
|
190
|
+
elif t.type != "footnote_block_open": items.append(("raw", part))
|
|
191
|
+
return items
|
|
192
|
+
|
|
193
|
+
def _caption(part:list[Token]) -> tuple[str, str, Token]|None:
|
|
194
|
+
"""
|
|
195
|
+
`(kind, number, text)` of a quote reading `> **Table 2.** ...` or `> **Rysunek 2.1.** ...`,
|
|
196
|
+
`None` for any other quote.
|
|
197
|
+
"""
|
|
198
|
+
inlines = [t for t in part if t.type == "inline"]
|
|
199
|
+
if len(inlines) != 1: return None
|
|
200
|
+
kids = [c for c in inlines[0].children or [] if c.type != "text" or c.content]
|
|
201
|
+
if len(kids) < 3 or kids[0].type != "strong_open" or kids[2].type != "strong_close": return None
|
|
202
|
+
m = _CAPTION.fullmatch(kids[1].content.strip())
|
|
203
|
+
if not m: return None
|
|
204
|
+
rest = kids[3:]
|
|
205
|
+
if rest and rest[0].type == "text": rest = [text(rest[0].content.lstrip())] + rest[1:]
|
|
206
|
+
kind = "table" if m["table"] else "figure"
|
|
207
|
+
return kind, m["number"], Token("inline", "", 0, children=rest)
|
|
208
|
+
|
|
209
|
+
def _structure(
|
|
210
|
+
items:list, title:str, fm:dict, journal:Journal,
|
|
211
|
+
) -> tuple[list[Block], dict, dict]:
|
|
212
|
+
"""Blocks of the text, and the abstracts and keywords taken out of it."""
|
|
213
|
+
levels = [int(p[0].tag[1]) for kind, p in items if kind == "heading"
|
|
214
|
+
and plain(p[1]).strip().lower() not in _ABSTRACT and plain(p[1]).strip() != title]
|
|
215
|
+
base = min(levels, default=2)
|
|
216
|
+
blocks, abstracts, keywords = [], {}, {}
|
|
217
|
+
mode, ack, dropped = None, False, False
|
|
218
|
+
i = 0
|
|
219
|
+
while i < len(items):
|
|
220
|
+
kind, part = items[i]
|
|
221
|
+
after = items[i + 1] if i + 1 < len(items) else ("", [])
|
|
222
|
+
i += 1
|
|
223
|
+
if kind == "heading":
|
|
224
|
+
name = plain(part[1]).strip()
|
|
225
|
+
mode = _ABSTRACT.get(name.lower())
|
|
226
|
+
dropped = bool(_DROPPED.fullmatch(name))
|
|
227
|
+
level = int(part[0].tag[1]) - base + 1
|
|
228
|
+
# What follows the references' name goes, as they come from the notes, but a thesis keeps
|
|
229
|
+
# a chapter of that name: they stand in its place
|
|
230
|
+
kept = dropped and journal.school is not None and level == 1
|
|
231
|
+
if mode or (dropped and not kept) or name == title: continue
|
|
232
|
+
ack = bool(_ACK.fullmatch(name))
|
|
233
|
+
blocks.append(Block("heading", part, part[1], level=level, ack=ack))
|
|
234
|
+
continue
|
|
235
|
+
if dropped: continue
|
|
236
|
+
if kind == "para":
|
|
237
|
+
m = _KEYWORDS.fullmatch(plain(part[1]))
|
|
238
|
+
if m:
|
|
239
|
+
lang = _KEYWORD_NAMES[m[1].lower()]
|
|
240
|
+
keywords[lang] = [w for w in re.split(r"\s*[;,]\s*", m[2].strip().rstrip(".")) if w]
|
|
241
|
+
mode = None
|
|
242
|
+
elif mode: abstracts.setdefault(mode, []).append(part[1])
|
|
243
|
+
else: blocks.append(Block("para", part, ack=ack))
|
|
244
|
+
continue
|
|
245
|
+
caption = _caption(after[1]) if after[0] == "caption" else None
|
|
246
|
+
if kind == "image" and caption and caption[0] == "figure":
|
|
247
|
+
blocks.append(Block("figure", part, caption[2], number=caption[1], ack=ack))
|
|
248
|
+
i += 1
|
|
249
|
+
elif kind == "image" and journal.school is not None and pages_file(part):
|
|
250
|
+
blocks.append(Block("pages", part, ack=ack)) # a thesis sets in a PDF that has no caption
|
|
251
|
+
elif kind == "caption" and after[0] == "table" and _caption(part)[0] == "table":
|
|
252
|
+
_, number, words = _caption(part)
|
|
253
|
+
blocks.append(Block("table", after[1], words, number=number, ack=ack))
|
|
254
|
+
i += 1
|
|
255
|
+
elif kind in ("wide", "narrow", "math"): blocks.append(Block(kind, part, ack=ack))
|
|
256
|
+
else: blocks.append(Block("raw", part, ack=ack))
|
|
257
|
+
if not keywords and fm.get("keywords"):
|
|
258
|
+
words = fm["keywords"]
|
|
259
|
+
keywords[journal.lang] = words if isinstance(words, list) else str(words).split(", ")
|
|
260
|
+
return blocks, abstracts, keywords
|
|
261
|
+
|
|
262
|
+
def pages_file(tokens:list[Token]) -> str:
|
|
263
|
+
"""
|
|
264
|
+
The PDF a paragraph of one image names, ``, whose pages a document sets in
|
|
265
|
+
as its own, as a dissertation sets in its published articles: `""` for any other paragraph.
|
|
266
|
+
"""
|
|
267
|
+
images = [c for t in tokens if t.type == "inline" for c in t.children or []
|
|
268
|
+
if c.type == "image"]
|
|
269
|
+
src = images[0].attrGet("src") if len(images) == 1 else ""
|
|
270
|
+
return src if str(src).lower().endswith(".pdf") else ""
|
|
271
|
+
|
|
272
|
+
def _authors(fm:dict, people) -> tuple[list[Author], dict]:
|
|
273
|
+
"""
|
|
274
|
+
Authors of `author:`, as the engine's `people` reads it, and the places of `affiliations:`.
|
|
275
|
+
One with no `aff` is with the issuer of the document, `entity` at `address`.
|
|
276
|
+
"""
|
|
277
|
+
places = {str(k): str(v) for k, v in (fm.get("affiliations") or {}).items()}
|
|
278
|
+
issuer = ", ".join(str(fm[k]) for k in ("entity", "address") if fm.get(k))
|
|
279
|
+
authors = []
|
|
280
|
+
for p in people(fm.get("author")):
|
|
281
|
+
aff = tuple(_listed(p.get("aff"))) or (("entity",) if issuer else ())
|
|
282
|
+
authors.append(Author(p["name"], aff, str(p.get("email") or ""), str(p.get("orcid") or ""),
|
|
283
|
+
bool(p.get("corresponding"))))
|
|
284
|
+
if any("entity" in a.aff for a in authors): places.setdefault("entity", issuer)
|
|
285
|
+
return authors, places
|
|
286
|
+
|
|
287
|
+
def _listed(value) -> list[str]:
|
|
288
|
+
if isinstance(value, list): return [str(v).strip() for v in value]
|
|
289
|
+
return [v.strip() for v in str(value).split(",")] if value else []
|
|
290
|
+
|
|
291
|
+
def _published(fm:dict, facts:dict) -> dict:
|
|
292
|
+
"""`published:` read into its fields as written, and `doi:`, for the journal's lines."""
|
|
293
|
+
out = {"doi": str(fm["doi"])} if fm.get("doi") else {}
|
|
294
|
+
if not fm.get("published"): return out
|
|
295
|
+
m = _PUBLISHED.fullmatch(str(fm["published"]).strip())
|
|
296
|
+
if m: return m.groupdict() | out
|
|
297
|
+
facts["published"] = str(fm["published"])
|
|
298
|
+
return out
|
|
299
|
+
|
|
300
|
+
def _name(name:str) -> str:
|
|
301
|
+
"""Pattern of `name`, its given names in full or as initials: `Anna Nowak`, `A. Nowak`."""
|
|
302
|
+
*given, surname = name.split() or [""]
|
|
303
|
+
if not given: return re.escape(surname)
|
|
304
|
+
space = r"\s+" # a no-break space too
|
|
305
|
+
full = space.join(map(re.escape, given))
|
|
306
|
+
initials = r"\s*".join(rf"{re.escape(g[0])}\." for g in given)
|
|
307
|
+
return rf"(?:{full}|{initials}){space}{re.escape(surname)}"
|
|
308
|
+
|
|
309
|
+
def _unnamed(piece:Token, names:re.Pattern) -> Token:
|
|
310
|
+
"""`piece` with `names` as `Anonymous`, the authors of one reference as one."""
|
|
311
|
+
return inline([text(_ANONYMOUS.sub("Anonymous", names.sub("Anonymous", c.content)))
|
|
312
|
+
if c.type == "text" else c for c in piece.children or []])
|
|
313
|
+
|
|
314
|
+
#---------------------------------------------------------------------------------------- Citations
|
|
315
|
+
|
|
316
|
+
def _notes(tokens:list[Token]) -> dict[str, Token]:
|
|
317
|
+
"""Footnote definitions by label: the inline of the first paragraph of each."""
|
|
318
|
+
notes, label = {}, None
|
|
319
|
+
for t in tokens:
|
|
320
|
+
if t.type == "footnote_open": label = (t.meta or {}).get("label")
|
|
321
|
+
elif t.type == "inline" and label is not None and label not in notes: notes[label] = t
|
|
322
|
+
elif t.type == "footnote_close": label = None
|
|
323
|
+
return notes
|
|
324
|
+
|
|
325
|
+
def _inlines(blocks:list[Block], abstracts:dict) -> list[Token]:
|
|
326
|
+
"""Every inline token in reading order: abstracts, then the blocks, captions where they stand."""
|
|
327
|
+
out = [t for parts in abstracts.values() for t in parts]
|
|
328
|
+
for b in blocks:
|
|
329
|
+
if b.kind == "figure": out += [t for t in b.tokens if t.type == "inline"] + [b.text]
|
|
330
|
+
elif b.kind == "table": out += [b.text] + [t for t in b.tokens if t.type == "inline"]
|
|
331
|
+
elif b.kind == "heading": out.append(b.text)
|
|
332
|
+
else: out += [t for t in b.tokens if t.type == "inline"]
|
|
333
|
+
return out
|
|
334
|
+
|
|
335
|
+
def _cite(
|
|
336
|
+
blocks:list[Block], abstracts:dict, notes:dict, journal:Journal, facts:dict,
|
|
337
|
+
) -> list[Ref]:
|
|
338
|
+
"""
|
|
339
|
+
Footnote marks turned into reference numbers in the order of first citation.
|
|
340
|
+
A footnote naming several references gives each its own number.
|
|
341
|
+
"""
|
|
342
|
+
refs, numbers = [], {}
|
|
343
|
+
def number(label:str) -> list[int]:
|
|
344
|
+
if label not in numbers:
|
|
345
|
+
numbers[label] = []
|
|
346
|
+
for entry in entries(notes[label]):
|
|
347
|
+
refs.append(Ref(len(refs) + 1, format_ref(entry, journal.cite, journal.lang, journal.dash),
|
|
348
|
+
has_doi(entry)))
|
|
349
|
+
numbers[label].append(len(refs))
|
|
350
|
+
return numbers[label]
|
|
351
|
+
for piece in _inlines(blocks, abstracts):
|
|
352
|
+
out, group = [], []
|
|
353
|
+
for c in (piece.children or []) + [None]:
|
|
354
|
+
if c is not None and c.type == "footnote_ref":
|
|
355
|
+
group.append((c.meta or {}).get("label"))
|
|
356
|
+
continue
|
|
357
|
+
if group:
|
|
358
|
+
cited = sorted({n for g in group for n in number(g)})
|
|
359
|
+
_bind_mark(out, marks(cited, journal.cite_sep, journal.cite_joined))
|
|
360
|
+
group = []
|
|
361
|
+
if c is not None: out.append(c)
|
|
362
|
+
piece.children = out
|
|
363
|
+
facts["uncited"] = [label for label in notes if label not in numbers]
|
|
364
|
+
return refs
|
|
365
|
+
|
|
366
|
+
def marks(numbers:list[int], sep:str, joined:bool=False) -> str:
|
|
367
|
+
"""
|
|
368
|
+
`[1],[2]`, and a run of three or more as `[3]` to `[5]` joined by an en dash,
|
|
369
|
+
or `joined` in one bracket: `[1, 2]`, `[3–5]`, `[6, 10–12, 17]`.
|
|
370
|
+
"""
|
|
371
|
+
runs, start = [], 0 # `(first, last)` of each run of numbers, a lone one its own run
|
|
372
|
+
for i in range(1, len(numbers) + 1):
|
|
373
|
+
if i == len(numbers) or numbers[i] != numbers[i - 1] + 1:
|
|
374
|
+
run = numbers[start:i]
|
|
375
|
+
runs += [(run[0], run[-1])] if len(run) > 2 else [(n, n) for n in run]
|
|
376
|
+
start = i
|
|
377
|
+
if joined: return "[" + sep.join(str(a) if a == b else f"{a}{EN_DASH}{b}" for a, b in runs) + "]"
|
|
378
|
+
return sep.join(f"[{a}]" if a == b else f"[{a}]{EN_DASH}[{b}]" for a, b in runs)
|
|
379
|
+
|
|
380
|
+
def _bind_mark(out:list[Token], mark:str):
|
|
381
|
+
"""Append citation `mark`, bound to the word before it by a no-break space."""
|
|
382
|
+
if out and out[-1].type == "text":
|
|
383
|
+
before = out[-1].content
|
|
384
|
+
space = "" if before.endswith(("(", "[")) or not before else NBSP
|
|
385
|
+
out[-1] = text(before.rstrip(" ") + space)
|
|
386
|
+
elif out: out.append(text(NBSP))
|
|
387
|
+
out.append(text(mark))
|
|
388
|
+
|
|
389
|
+
#---------------------------------------------------------------------------------------- Numbering
|
|
390
|
+
|
|
391
|
+
def _number(blocks:list[Block], journal:Journal, facts:dict):
|
|
392
|
+
"""
|
|
393
|
+
Labels of headings, figures, tables and formulas,
|
|
394
|
+
mentions of figures and tables renumbered to match, and of formulas named by their label.
|
|
395
|
+
A chapter the journal sets without a number, as `Wstęp` of a thesis, and its sections take none.
|
|
396
|
+
Numbered within chapters, a figure, table or formula of a numbered one bears its number: `2.1`,
|
|
397
|
+
and one outside them counts apart: `1`, `2`.
|
|
398
|
+
A source may number them either way, through the text or by chapter.
|
|
399
|
+
"""
|
|
400
|
+
depth = max(len(journal.heads), 1)
|
|
401
|
+
counts, maps, totals = [0] * depth, {"figure": {}, "table": {}}, {"figure": 0, "table": 0}
|
|
402
|
+
shown = {"figure": {}, "table": {}} # each number in reading order as set: `II`, `2.1`
|
|
403
|
+
within = {"figure": 0, "table": 0, "math": 0} # of the chapter
|
|
404
|
+
apart = dict(within) # outside the numbered chapters
|
|
405
|
+
equation, section, opened, loose = 0, False, not journal.dropcap, False
|
|
406
|
+
def counted(kind:str, n:int) -> str:
|
|
407
|
+
"""How the `n`th of `kind` in reading order is numbered."""
|
|
408
|
+
if kind == "table" and journal.table_roman: return roman(n)
|
|
409
|
+
if not journal.by_chapter: return str(n)
|
|
410
|
+
if loose or not counts[0]:
|
|
411
|
+
apart[kind] += 1
|
|
412
|
+
return str(apart[kind])
|
|
413
|
+
within[kind] += 1
|
|
414
|
+
return f"{counts[0]}.{within[kind]}"
|
|
415
|
+
for b in blocks:
|
|
416
|
+
if b.kind == "heading" and not b.ack:
|
|
417
|
+
if b.level == 1:
|
|
418
|
+
loose = unnumbered(b, journal)
|
|
419
|
+
within = dict.fromkeys(within, 0)
|
|
420
|
+
if loose: continue
|
|
421
|
+
level = min(b.level, depth)
|
|
422
|
+
counts[level - 1] += 1
|
|
423
|
+
counts[level:] = [0] * (depth - level)
|
|
424
|
+
b.label = _head_label(counts, level, journal)
|
|
425
|
+
section = section or level == 1
|
|
426
|
+
elif b.kind in maps:
|
|
427
|
+
totals[b.kind] += 1
|
|
428
|
+
n = b.n = totals[b.kind]
|
|
429
|
+
maps[b.kind].setdefault(b.number, n)
|
|
430
|
+
label = shown[b.kind][n] = counted(b.kind, n)
|
|
431
|
+
if b.number not in (str(n), label): # neither as read nor as set
|
|
432
|
+
facts["renumbered"].append(f"{b.kind} {b.number} → {label}")
|
|
433
|
+
if b.kind == "figure":
|
|
434
|
+
b.label = f"{journal.figure} {label}."
|
|
435
|
+
facts["images"] += [a for t in b.tokens for c in t.children or [] if c.type == "image"
|
|
436
|
+
for a in [c.attrGet("src")]]
|
|
437
|
+
else:
|
|
438
|
+
b.label = f"{journal.table} {label}"
|
|
439
|
+
if not journal.table_split: b.label += "."
|
|
440
|
+
elif b.kind == "math":
|
|
441
|
+
source = b.tokens[0].content # its label read and left out, which no engine draws
|
|
442
|
+
names, source = _LABEL.findall(source), _LABEL.sub("", source)
|
|
443
|
+
if journal.equations:
|
|
444
|
+
equation += 1
|
|
445
|
+
number = counted("math", equation)
|
|
446
|
+
source = f"{source.rstrip()} \\tag{{{number}}}"
|
|
447
|
+
facts["doubled"] += [name for name in names if name in facts["labels"]]
|
|
448
|
+
facts["labels"].update(dict.fromkeys(names, number))
|
|
449
|
+
b.tokens[0].content = source
|
|
450
|
+
elif b.kind == "para" and section and not opened:
|
|
451
|
+
b.first = opened = True
|
|
452
|
+
mention = lambda m: _mention(m, maps, shown, facts)
|
|
453
|
+
for piece in _inlines(blocks, {}):
|
|
454
|
+
for c in piece.children or []:
|
|
455
|
+
if c.type == "text": c.content = _MENTION.sub(mention, c.content)
|
|
456
|
+
|
|
457
|
+
def _formulas(blocks:list[Block], abstracts:dict, facts:dict):
|
|
458
|
+
"""
|
|
459
|
+
Mentions of labelled formulas as their numbers, wherever they are written:
|
|
460
|
+
in the words, in a formula of the text and in the abstracts,
|
|
461
|
+
a `$...$` holding one mention alone set as words, any other kept a formula.
|
|
462
|
+
A formula in a list or quote takes no number, so its label is left out and noted.
|
|
463
|
+
"""
|
|
464
|
+
refer = lambda m: _refer(m, facts)
|
|
465
|
+
nested = [t for b in blocks if b.kind != "math" for t in b.tokens if t.type == "math_block"]
|
|
466
|
+
for t in nested:
|
|
467
|
+
facts["apart"].update(_LABEL.findall(t.content))
|
|
468
|
+
t.content = _LABEL.sub("", t.content)
|
|
469
|
+
for t in [t for b in blocks for t in b.tokens if t.type == "math_block"]:
|
|
470
|
+
t.content = _EQREF.sub(refer, t.content)
|
|
471
|
+
for piece in _inlines(blocks, abstracts):
|
|
472
|
+
for c in piece.children or []:
|
|
473
|
+
if c.type == "text": c.content = _EQREF.sub(refer, c.content)
|
|
474
|
+
elif c.type == "math_inline" and _EQREF.fullmatch(c.content.strip()):
|
|
475
|
+
c.type, c.content = "text", _EQREF.sub(refer, c.content.strip())
|
|
476
|
+
elif c.type == "math_inline": c.content = _EQREF.sub(refer, c.content)
|
|
477
|
+
|
|
478
|
+
def unnumbered(b:Block, journal:Journal) -> bool:
|
|
479
|
+
"""
|
|
480
|
+
Whether chapter `b` stands without a number: one the journal so names, an appendix, a part,
|
|
481
|
+
or the place of a thesis's references.
|
|
482
|
+
"""
|
|
483
|
+
name = plain(b.text).strip()
|
|
484
|
+
return references(b, journal) or any(p and re.fullmatch(p, name, re.I)
|
|
485
|
+
for p in (journal.unnumbered, journal.appendix, journal.parts))
|
|
486
|
+
|
|
487
|
+
def part(b:Block, journal:Journal) -> bool:
|
|
488
|
+
"""Whether `b` opens a part of the text, as `Część I. Autoreferat`, the contents set apart."""
|
|
489
|
+
return (b.kind == "heading" and b.level == 1 and bool(journal.parts)
|
|
490
|
+
and bool(re.fullmatch(journal.parts, plain(b.text).strip(), re.I)))
|
|
491
|
+
|
|
492
|
+
def references(b:Block, journal:Journal) -> bool:
|
|
493
|
+
"""Whether `b` is a thesis chapter named as references, which stand in its place."""
|
|
494
|
+
return (b.kind == "heading" and b.level == 1 and journal.school is not None
|
|
495
|
+
and bool(_DROPPED.fullmatch(plain(b.text).strip())))
|
|
496
|
+
|
|
497
|
+
def list_of(b:Block, journal:Journal) -> str:
|
|
498
|
+
"""
|
|
499
|
+
`figure` or `table` for a thesis chapter named as their list, `Wykaz rysunków` or
|
|
500
|
+
`List of tables`, which lists them where it stands; `""` for any other block.
|
|
501
|
+
"""
|
|
502
|
+
if b.kind != "heading" or b.level != 1 or journal.school is None: return ""
|
|
503
|
+
name = plain(b.text).strip()
|
|
504
|
+
return next((kind for kind, names in _LISTS.items() if names.fullmatch(name)), "")
|
|
505
|
+
|
|
506
|
+
def appendix(b:Block, journal:Journal) -> bool:
|
|
507
|
+
"""Whether `b` opens a chapter the journal sets after the references, as an appendix."""
|
|
508
|
+
return (b.kind == "heading" and b.level == 1 and bool(journal.appendix)
|
|
509
|
+
and bool(re.fullmatch(journal.appendix, plain(b.text).strip(), re.I)))
|
|
510
|
+
|
|
511
|
+
def _head_label(counts:list[int], level:int, journal:Journal) -> str:
|
|
512
|
+
"""`II.`, `B.`, `2)` or, nested, `2.1.`: the number the level's `Head` asks for."""
|
|
513
|
+
fmt = journal.head(level).number
|
|
514
|
+
if not fmt: return ""
|
|
515
|
+
if journal.nested: return ".".join(str(n) for n in counts[:level]) + fmt[1:]
|
|
516
|
+
return counter(counts[level - 1], fmt[0]) + fmt[1:]
|
|
517
|
+
|
|
518
|
+
def _mention(m:re.Match, maps:dict, shown:dict, facts:dict) -> str:
|
|
519
|
+
"""
|
|
520
|
+
`Tables 1 and 5` as the journal numbers the tables they mean, `shown` by their numbers
|
|
521
|
+
in reading order, the word bound to the first.
|
|
522
|
+
"""
|
|
523
|
+
word = m["word"]
|
|
524
|
+
kind = "table" if m["table"] else "figure"
|
|
525
|
+
parts = re.split(f"({_NUMBER})", m["list"]) # what comes before each number, then the number
|
|
526
|
+
out, last = [], None
|
|
527
|
+
for k in range(1, len(parts), 2):
|
|
528
|
+
number, ranged = parts[k], last is not None and bool(_RANGE.fullmatch(parts[k - 1]))
|
|
529
|
+
if ranged: # a range names every number inside it
|
|
530
|
+
facts["mentioned"] |= {(kind, maps[kind][x]) for x in _inside(last, number)
|
|
531
|
+
if x in maps[kind]}
|
|
532
|
+
n = maps[kind].get(number)
|
|
533
|
+
if n is None: facts["missing"].append(f"{word} {number}")
|
|
534
|
+
else: facts["mentioned"].add((kind, n))
|
|
535
|
+
out.append((EN_DASH if ranged else parts[k - 1]) + (shown[kind][n] if n else number))
|
|
536
|
+
last = number
|
|
537
|
+
return f"{word}{NBSP}{''.join(out)}"
|
|
538
|
+
|
|
539
|
+
def _refer(m:re.Match, facts:dict) -> str:
|
|
540
|
+
"""
|
|
541
|
+
A mention of the formula its label names, as its number: in brackets for `eqref`, `(2.1)`,
|
|
542
|
+
alone for `ref`, `2.1`; one naming no formula stays as written, for the preflight.
|
|
543
|
+
"""
|
|
544
|
+
number = facts["labels"].get(m[2])
|
|
545
|
+
if number is None:
|
|
546
|
+
facts["unlabelled"].append((m[0], m[2]))
|
|
547
|
+
return m[0]
|
|
548
|
+
facts["referred"].add(m[2])
|
|
549
|
+
return f"({number})" if m[1] else number
|
|
550
|
+
|
|
551
|
+
def _inside(first:str, last:str) -> list[str]:
|
|
552
|
+
"""Numbers a range names between its ends: `2`–`5` names 3 and 4, `2.1`–`2.4` 2.2 and 2.3."""
|
|
553
|
+
*head, low = first.split(".")
|
|
554
|
+
*tail, high = last.split(".")
|
|
555
|
+
if head != tail: return []
|
|
556
|
+
return [".".join(head + [str(x)]) for x in range(int(low) + 1, int(high))]
|
|
557
|
+
|
|
558
|
+
def _run_in(blocks:list[Block], journal:Journal):
|
|
559
|
+
"""Headings of a run-in level opening the paragraph after them, in the level's face."""
|
|
560
|
+
for b in [b for b in blocks if b.kind == "heading" and journal.head(b.level).run_in]:
|
|
561
|
+
i = blocks.index(b)
|
|
562
|
+
after = blocks[i + 1] if i + 1 < len(blocks) else None
|
|
563
|
+
if after is None or after.kind != "para": continue
|
|
564
|
+
mode = journal.head(b.level).mode
|
|
565
|
+
tags = (["strong"] if "Bold" in mode else []) + (["em"] if "Italic" in mode else [])
|
|
566
|
+
lead = f"{b.label} {plain(b.text).strip()}: ".lstrip()
|
|
567
|
+
after.tokens[1].children = [Token(f"{t}_open", t, 1) for t in tags] + [text(lead)] + [
|
|
568
|
+
Token(f"{t}_close", t, -1) for t in reversed(tags)] + (after.tokens[1].children or [])
|
|
569
|
+
blocks.remove(b)
|
|
570
|
+
|
|
571
|
+
#--------------------------------------------------------------------------------------- Typography
|
|
572
|
+
|
|
573
|
+
def _unstopped(inline:Token):
|
|
574
|
+
"""A caption without the full stop it ends with, for a journal whose captions end without one."""
|
|
575
|
+
texts = [c for c in inline.children or [] if c.type == "text" and c.content.strip()]
|
|
576
|
+
last = texts[-1] if texts else None
|
|
577
|
+
if last is None: return
|
|
578
|
+
words = last.content.rstrip()
|
|
579
|
+
if words.endswith(".") and not words.endswith(".."): last.content = words[:-1]
|
|
580
|
+
|
|
581
|
+
def _polish(inline:Token, journal:Journal, facts:dict, bold:bool, text:bool):
|
|
582
|
+
"""
|
|
583
|
+
Typography of the journal's language on the text of `inline`, code left alone,
|
|
584
|
+
links kept where the journal keeps them, and bold where `bold` says.
|
|
585
|
+
Bold set plain is counted for the preflight, and bold kept in the `text`, outside tables.
|
|
586
|
+
"""
|
|
587
|
+
dropped = set() if journal.links else {"link_open", "link_close"}
|
|
588
|
+
if not bold: dropped |= {"strong_open", "strong_close"}
|
|
589
|
+
out, prev = [], " "
|
|
590
|
+
for c in inline.children or []:
|
|
591
|
+
if c.type in dropped:
|
|
592
|
+
facts["plain"] += c.type == "strong_open"
|
|
593
|
+
continue
|
|
594
|
+
facts["bold"] += text and c.type == "strong_open"
|
|
595
|
+
if c.type == "text":
|
|
596
|
+
c.content = bind(smarten(c.content, journal.lang, prev, journal.dash))
|
|
597
|
+
prev = c.content[-1:] or prev
|
|
598
|
+
elif c.type == "code_inline": prev = "x"
|
|
599
|
+
out.append(c)
|
|
600
|
+
inline.children = out
|