diff --git a/README.md b/README.md
index 5755f90..837ea8d 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ Code blocks with a language are syntax colored when [fastpylight](https://github
## Styling
-The generated document uses named styles, never inline formatting, so appearance is controlled by restyling. Prose paragraphs get Body Text (First Paragraph directly after a heading or similar block, following pandoc's convention), and the other styles are the ones you would expect: heading 1 through 6, Quote, Source Code, Verbatim Char, Hyperlink, List Paragraph, Compact (table cells), Definition Term, Definition, caption, footnote styles, and Table Grid (plus the author-selectable Borderless Table).
+The generated document uses named styles, never inline formatting, so appearance is controlled by restyling. A markdown h1 is the document title: it gets Word's Title style and never joins heading numbering, and h2 through h6 map to heading 1 through 5. Prose paragraphs get Body Text (First Paragraph directly after a heading or similar block, following pandoc's convention), and the other styles are the ones you would expect: Quote, Source Code, Verbatim Char, Hyperlink, List Paragraph, Compact (table cells), Definition Term, Definition, caption, footnote styles, and Table Grid (plus the author-selectable Borderless Table).
Pass `reference='mydoc.docx'` to use your own document's styles instead of the built-in template, exactly like pandoc's `--reference-doc`. `reference` may also be a list: the first entry supplies the document (page setup, fonts, and all base styles), and each later entry contributes just its styles, replacing same-named earlier ones - either another `.docx`, or a fastpylight theme name such as `'dracula'`, which generates the code-color styles on the fly. The default is the built-in template plus `'github_light'`; pass a bare reference for plain uncolored code, or `mdhtml2docx.styles.theme_ref('dracula', 'dracula.docx')` to write a theme's styles as a standalone docx you can inspect or tweak. A `custom-style="Name"` attribute (from `{custom-style="Name"}` in Markdown) applies that style from your reference doc; if the style is missing, a stub is injected and a warning returned. A plain class like `{.note}` applies a style only when your reference doc defines one named `note`, and is otherwise ignored. Both work on tables too: a table whose `custom-style` or class names a table style in the reference doc uses it in place of Table Grid - the built-in template ships `Borderless Table` (no gridlines, for signature blocks and other layout tables).
diff --git a/_data/empty.docx b/_data/empty.docx
new file mode 100644
index 0000000..292ef49
Binary files /dev/null and b/_data/empty.docx differ
diff --git a/mdhtml2docx/convert.py b/mdhtml2docx/convert.py
index 21cc27a..28ffd10 100644
--- a/mdhtml2docx/convert.py
+++ b/mdhtml2docx/convert.py
@@ -207,12 +207,12 @@ def ref_prefix(self, el, fmt, plural=False):
def ref_fld(self, el, fmt):
"""REF/PAGEREF field for a cross-reference `a`, with a cached placeholder Word replaces on update.
- Heading/paragraph targets number via `\\w`; caption targets return their bookmarked 'Label N' text
+ Heading, paragraph, and span targets number via `\\w`; caption targets return their bookmarked 'Label N' text
(or the number-only `_n` bookmark for bare/leaf/rel refs), so `\\w` never applies to them."""
tgt = (_get(el, 'href') or '#')[1:]
tokens = ref_tokens(_get(el, 'data-ref'))
if tgt not in self.reftarget:
- raise ValueError(f'cross-reference target #{tgt} not found (targets are headings, paragraphs, figures, and tables with ids)')
+ raise ValueError(f'cross-reference target #{tgt} not found (targets are headings, paragraphs, spans, figures, and tables with ids)')
kind = ref_variant(tokens)
nm, self.has_fields = self.bkname(tgt), True
if kind == 'page': instr, cached = rf' PAGEREF {nm} \h ', '#'
@@ -247,11 +247,11 @@ def custom_style(self, el, kind):
return next((self.refstyles[c.lower()] for c in _classes(el) if c.lower() in self.refstyles), None)
def span(self, el, fmt):
- "Inline span: math -> inline m:oMath zone (linear source, dialect-agnostic), custom style -> rStyle, else transparent"
+ "Inline span: math -> inline m:oMath zone (linear source, dialect-agnostic), custom style -> rStyle, else transparent; an id becomes a bookmark (REF target)"
if _get(el, 'data-refs') is not None: return self.ref_group(el, fmt)
if 'math' in _classes(el): return [self.omath(el)]
- if sid := self.custom_style(el, 'character'): return self.runs(el, fmt | {'rstyle': sid})
- return self.runs(el, fmt)
+ if sid := self.custom_style(el, 'character'): return self.bookmark(el, self.runs(el, fmt | {'rstyle': sid}))
+ return self.bookmark(el, self.runs(el, fmt))
def omath(self, el):
"An m:oMath zone holding `el`'s text as linear-format math runs"
@@ -728,7 +728,8 @@ def numbering_xml(self):
an.append(E('w:lvl', {'w:ilvl': i}, # chkstyle: ignore-node
E('w:start', {'w:val': 1}), E('w:numFmt', {'w:val': fmt}),
E('w:pStyle', {'w:val': f'Heading{i + 1}'}) if i < 6 else None,
- E('w:lvlText', {'w:val': txt}), E('w:lvlJc', {'w:val': 'left'})))
+ E('w:lvlText', {'w:val': txt}), E('w:lvlJc', {'w:val': 'left'}),
+ E('w:pPr', E('w:ind', {'w:left': 360 + 360 * i, 'w:hanging': 360 + 360 * i})))) # number at the margin, text stair-stepped per level
root.append(an)
for e in self.xabs: root.append(e)
for e in tnums: root.append(e)
@@ -851,7 +852,7 @@ def harvest_footnotes(self, els):
self.fndefs.update({_get(li, 'id'): li for sec in fn for li in _walk(sec) if _tag(li) == 'li' and _get(li, 'id')})
return body
- BOOKMARKABLE = {'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'}
+ BOOKMARKABLE = {'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'}
def to_docx(self, mdhtml, dest):
root = parse_frag(mdhtml)
@@ -923,7 +924,7 @@ def convert(mdhtml, dest, reference=None, base=None, reftypes=None, number_headi
resolve against `base` ('.'). Cross-references (`data-ref` anchors from Markdown `[@sec-x]`) become
live REF fields; `reftypes` maps type tokens to (singular, plural) prefix words beyond the built-in
`sec`, and `number_headings` (a styles.SCHEMES name such as 'legal', or a {lvlText: numFmt} dict, one entry per heading level)
- numbers the headings via a multilevel list so `\\w` fields resolve. Template tokens are dropped
+ numbers the headings via a multilevel list so `\\w` fields resolve; h1 is the unnumbered document title (Title style), so scheme level 1 is h2. Template tokens are dropped
unless `tmpl` is given: a callable taking the token node dict (`mdhtml.export.tmpl_node`: `body`,
`syntax`, `form`, `kind`, `name`, `inverted`) and returning a str for a literal text run,
`('field', instr)` for a live field, `('control', name)` for an interactive plain-text content
diff --git a/mdhtml2docx/styles.py b/mdhtml2docx/styles.py
index 722aeda..98c309b 100644
--- a/mdhtml2docx/styles.py
+++ b/mdhtml2docx/styles.py
@@ -5,7 +5,7 @@
STYLE_MAP = dict( # chkstyle: ignore-node
body='Body Text', firstpara='First Paragraph', blockquote='Quote', codeblock='Source Code', codeinline='Verbatim Char',
- h1='heading 1', h2='heading 2', h3='heading 3', h4='heading 4', h5='heading 5', h6='heading 6',
+ h1='Title', h2='heading 1', h3='heading 2', h4='heading 3', h5='heading 4', h6='heading 5',
compact='Compact', hyperlink='Hyperlink', list='List Paragraph', dt='Definition Term', dd='Definition',
caption='caption', footnotetext='footnote text', footnoteref='footnote reference', table='Table Grid')
diff --git a/mdhtml2docx/templates/reference.docx b/mdhtml2docx/templates/reference.docx
index 8b19395..dd53425 100644
Binary files a/mdhtml2docx/templates/reference.docx and b/mdhtml2docx/templates/reference.docx differ
diff --git a/tests/test_convert.py b/tests/test_convert.py
index aa59579..2159a66 100644
--- a/tests/test_convert.py
+++ b/tests/test_convert.py
@@ -1,4 +1,4 @@
-import base64, pytest, subprocess, zipfile
+import base64, pytest, re, subprocess, zipfile
from pathlib import Path
from fastcore.test import test_eq as teq, test as tt, test_fail as tfail
@@ -9,6 +9,7 @@
from mdhtml.tools import SAMPLE_MD, sample_md
from mdhtml2docx.convert import convert, mustache_fields
from mdhtml2docx.validate import fast_checks
+from mdhtml2docx.styles import ref_path
def pandoc(path, to='markdown'):
@@ -63,11 +64,11 @@ def test_basic_blocks(tmp_path):
teq(warns, [])
teq(fast_checks(out), 'valid')
md = pandoc(out)
- for s in ('# Title', '## Sub *title*', '`f(x)`', '[fast.ai](https://fast.ai/)',
+ for s in ('# Sub *title*', '`f(x)`', '[fast.ai](https://fast.ai/)', # no '# Title': the h1 is Title style, which pandoc lifts to metadata (its bookmark still resolves below)
'> Quoted line.', 'def f(x):', 'return x'): tt(s, md, in_)
- # pandoc resolves the anchor against the heading bookmark and rewrites it to the heading's
- # auto-identifier, so this line proves the internal link wiring survived the round trip
- tt('[the title](#title)', md, in_)
+ # the internal link survived the round trip: pandoc keeps the raw bookmark anchor, since a
+ # Title-styled target is not a heading (heading targets get rewritten to auto-identifiers)
+ tt('[the title](#top)', md, in_)
def test_lists(tmp_path):
@@ -89,6 +90,34 @@ def test_lists(tmp_path):
tt('- [ ] todo', lines, in_)
+def test_default_reference():
+ "The bundled reference carries the house look: TNR 11 defaults, justified prose, Title and Centered styles, a page-number footer"
+ z = zipfile.ZipFile(ref_path())
+ styles = z.read('word/styles.xml').decode()
+ for s in ('w:styleId="Title"', 'w:styleId="Centered"', 'Times New Roman'): tt(s, styles, in_)
+ tt('w:val="both"', re.search(r'