Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ Cross-references become plain text ("See Section 1.(a)"), with the same `reftype

Inline constructs are recognized at any nesting depth with the parser's own grammar, so code spans, links, and escapes are honored, and `use {braces} freely` stays literal. Block constructs are rewritten wherever their lines carry no container marker, which includes fenced divs; a heading or table caption inside a blockquote or list passes through unchanged, with a warning when it needed numbering or stripping.

`fill_tokens(src, values, classify, templates)` is the companion filler: it resolves template tokens from a plain dict and touches nothing else, so the result is still-symbolic Markdown ready for any exporter. The `classify` callable defines the grammar, mapping a token's `(body, syntax)` to `('var', name)`, `('open', name, inverted)`, or `('close', name)`. Variables take `str(values[name])`; sections keep or drop their span by the value's truthiness (kept sections just lose their markers; no iteration). By default a field missing in either direction raises; with `strict=False` the mismatches land in `.warnings` and unfilled variables stay in place, so a document can be filled in stages. `mdhtml.mustache.fill_md` and `mdhtml.jinja.fill_md` are the shipped instantiations - mustache's classifier reads `#`/`^`/`/` sigils from bodies, jinja's discriminates by delimiter pair (`{% if x %}`/`{% if not x %}`/`{% endif %}`) - and `examples/filldemo.py` shows the mustache one in use.
`fill_tokens(src, values, classify, templates)` is the companion filler: it resolves template tokens from a plain dict and touches nothing else, so the result is still-symbolic Markdown ready for any exporter. The `classify` callable defines the grammar, mapping a token's `(body, syntax)` to `('var', name)`, `('open', name, inverted)` or `('open', name, inverted, bind)`, or `('close', name)`. Variables take `str(values[name])`, with names as dotted paths resolved innermost-first through the enclosing sections' frames; sections keep or drop their span by the value's truthiness. `bind` says what a kept section pushes as the innermost frame: nothing for a pure conditional (jinja's `if`), `'.'` for the section's own value (mustache, so its fields are visible and `{{.}}` names it), or a name for `{bind: value}` (jinja's `for bind in name`). Under a binding open, a list value repeats the span once per item with that item's frame pushed, and an empty list drops it like any other falsy value. By default a field missing in either direction raises; with `strict=False` the mismatches land in `.warnings` and unfilled variables stay in place, so a document can be filled in stages. `mdhtml.mustache.fill_md` and `mdhtml.jinja.fill_md` are the shipped instantiations - mustache's classifier reads `#`/`^`/`/` sigils from bodies, jinja's discriminates by delimiter pair (`{% if x %}`/`{% if not x %}`/`{% endif %}` and `{% for x in xs %}`/`{% endfor %}`) - and `examples/filldemo.py` shows the mustache one in use, iterating a grant table and a list of contingencies.

Command-line usage (the `mdhtml` script is installed with the package):

Expand Down
8 changes: 6 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ notes in an `.ipynb`). It exercises the dialect features that matter across conv
- Headings with ids (`## Compensation {#sec-comp}`) referenced from *other* notes: single refs
(`[@sec-offer]`), a group (`[@sec-comp; @sec-equity; @sec-atwill]`), and custom text
(`[your cash compensation @sec-comp]`).
- Mustache template tokens: inline variables (`{{base_salary}}`) and block section markers
(`{{#equity.options}}` ... `{{/equity.options}}`).
- Mustache template tokens: inline variables (`{{base_salary}}`), conditional section markers
(`{{#equity.options}}` ... `{{/equity.options}}`), and list sections that repeat their span once
per item: a table of `{{#grants}}` rows, and `{{#contingencies}}` bullets naming each item with
`{{.}}`. Names inside a repeated span resolve innermost-first, so a row sees the grant's own
fields, falls back to the letter-wide `{{vesting_schedule}}`, and a grant carrying its own
`class_of_stock` shadows the outer one.
- A footnote, for id-namespacing to exercise.

## The build script
Expand Down
9 changes: 7 additions & 2 deletions examples/filldemo.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@
from mdhtml.mustache import MUSTACHE, fill_md

values = {'company_common_name': 'Acme Robotics, Inc.', 'candidate_name': 'Alex Rivera', 'job_title': 'Senior Research Engineer',
'base_salary': '$185,000', 'equity.options': True, 'shares_subject_to_option': '25,000', 'class_of_stock': 'Common Stock',
'vesting_schedule': 'four years, with a one-year cliff', 'equity.restricted_stock': False,
'base_salary': '$185,000', 'equity': {'options': True, 'restricted_stock': False},
'shares_subject_to_option': '25,000', 'class_of_stock': 'Common Stock',
'vesting_schedule': 'four years, with a one-year cliff',
'grants': [{'grant_date': 'September 1, 2026', 'shares': '25,000'},
{'grant_date': 'March 1, 2027', 'shares': '5,000', 'class_of_stock': 'Series A Preferred'}],
'contingencies': ['satisfactory completion of a background check', 'your signed confidentiality agreement',
'documentation of your eligibility to work'],
'offer_expiration_date': 'August 1, 2026', 'hiring_manager_name': 'Sam Devlin', 'offer_date': 'July 23, 2026'}

d = Path(__file__).parent
Expand Down
Binary file modified examples/legal_demo-bound.docx
Binary file not shown.
15 changes: 15 additions & 0 deletions examples/legal_demo-filled.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,23 @@ Your base salary will be $185,000 per year, paid on Acme Robotics, Inc.'s normal

Subject to approval by Acme Robotics, Inc.'s Board of Directors, you will be granted an option to purchase 25,000 shares of Common Stock at a strike price equal to fair market value on the date of grant. The option will vest over four years, with a one-year cliff.

Your grants under this offer are:

| Grant date | Shares | Class | Vesting |
|---|---|---|---|
| September 1, 2026 | 25,000 | Common Stock | four years, with a one-year cliff |
| March 1, 2027 | 5,000 | Series A Preferred | four years, with a one-year cliff |

Tax treatment is your responsibility; see also [your cash compensation @sec-comp].

## Contingencies {#sec-conting}

This offer is contingent on each of the following:

- satisfactory completion of a background check
- your signed confidentiality agreement
- documentation of your eligibility to work

## At-Will Employment {#sec-atwill}

Your employment with Acme Robotics, Inc. is at will: either you or the Company may end it at any time, with or without cause. Nothing in [@sec-comp] or [@sec-equity] changes that.
Expand Down
Binary file modified examples/legal_demo-filled.pdf
Binary file not shown.
Binary file modified examples/legal_demo-form.docx
Binary file not shown.
22 changes: 19 additions & 3 deletions examples/legal_demo-render.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 1. Offer of Employment

`{{company_common_name}}` (the "Company") is pleased to offer `{{candidate_name}}` the position of `{{job_title}}`. This letter summarizes the key terms: Sections 1.(a), 1.(b) and 1.(c).
`{{company_common_name}}` (the "Company") is pleased to offer `{{candidate_name}}` the position of `{{job_title}}`. This letter summarizes the key terms: Sections 1.(a), 1.(b) and 1.(d).

## (a) Compensation

Expand All @@ -18,9 +18,25 @@ Subject to approval by `{{company_common_name}}`'s Board of Directors, you will
Subject to Board approval, you will be granted the right to purchase `{{number_shares}}` shares of `{{class_of_stock}}` under a Restricted Stock Purchase Agreement, vesting over `{{vesting_schedule}}`.
`{{/equity.restricted_stock}}`

Your grants under this offer are:

| Grant date | Shares | Class | Vesting |
|---|---|---|---|
`{{#grants}}`
| `{{grant_date}}` | `{{shares}}` | `{{class_of_stock}}` | `{{vesting_schedule}}` |
`{{/grants}}`

Tax treatment is your responsibility; see also your cash compensation 1.(a).

## (c) At-Will Employment
## (c) Contingencies

This offer is contingent on each of the following:

`{{#contingencies}}`
- `{{.}}`
`{{/contingencies}}`

## (d) At-Will Employment

Your employment with `{{company_common_name}}` is at will: either you or the Company may end it at any time, with or without cause. Nothing in Section 1.(a) or Section 1.(b) changes that.

Expand All @@ -36,4 +52,4 @@ To accept, sign below by `{{offer_expiration_date}}`.
<tr><td>Date: `{{offer_date}}`</td><td>Date: `{{signature_date}}`</td></tr>
</table>

Please retain a copy of this letter for your records; the terms in Sections 1.(a), 1.(b) and 1.(c) are the entire agreement.
Please retain a copy of this letter for your records; the terms in Sections 1.(a), 1.(b) and 1.(d) are the entire agreement.
Binary file modified examples/legal_demo.docx
Binary file not shown.
25 changes: 21 additions & 4 deletions examples/legal_demo.html
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
<h1 id="sec-offer"><span class="heading-number">1.</span> Offer of Employment</h1>
<p><input name="company_common_name" placeholder="company_common_name"> (the "Company") is pleased to offer <input name="candidate_name" placeholder="candidate_name"> the position of <input name="job_title" placeholder="job_title">. This letter summarizes the key terms: <span>Sections <a href="#sec-comp">1.(a)</a>, <a href="#sec-equity">1.(b)</a> and <a href="#sec-atwill">1.(c)</a></span>.</p>
<p><input name="company_common_name" placeholder="company_common_name"> (the "Company") is pleased to offer <input name="candidate_name" placeholder="candidate_name"> the position of <input name="job_title" placeholder="job_title">. This letter summarizes the key terms: <span>Sections <a href="#sec-comp">1.(a)</a>, <a href="#sec-equity">1.(b)</a> and <a href="#sec-atwill">1.(d)</a></span>.</p>
<h2 id="sec-comp"><span class="heading-number">(a)</span> Compensation</h2>
<p>Your base salary will be <input name="base_salary" placeholder="base_salary"> per year, paid on <input name="company_common_name" placeholder="company_common_name">'s normal payroll schedule and subject to all withholdings required by law.<sup id="fnref-1"><a href="#fn-1" class="footnote-ref" role="doc-noteref">1</a></sup> Salary is reviewed annually as part of the process described in <a href="#sec-offer">Section 1.</a>.</p>
<h2 id="sec-equity"><span class="heading-number">(b)</span> Equity</h2>
<code>{{#equity.options}}</code><p>Subject to approval by <input name="company_common_name" placeholder="company_common_name">'s Board of Directors, you will be granted an option to purchase <input name="shares_subject_to_option" placeholder="shares_subject_to_option"> shares of <input name="class_of_stock" placeholder="class_of_stock"> at a strike price equal to fair market value on the date of grant. The option will vest over <input name="vesting_schedule" placeholder="vesting_schedule">.</p>
<code>{{/equity.options}}</code><code>{{#equity.restricted_stock}}</code><p>Subject to Board approval, you will be granted the right to purchase <input name="number_shares" placeholder="number_shares"> shares of <input name="class_of_stock" placeholder="class_of_stock"> under a Restricted Stock Purchase Agreement, vesting over <input name="vesting_schedule" placeholder="vesting_schedule">.</p>
<code>{{/equity.restricted_stock}}</code><p>Tax treatment is your responsibility; see also <a href="#sec-comp">your cash compensation 1.(a)</a>.</p>
<h2 id="sec-atwill"><span class="heading-number">(c)</span> At-Will Employment</h2>
<code>{{/equity.restricted_stock}}</code><p>Your grants under this offer are:</p>
<table>
<thead>
<tr><th>Grant date</th><th>Shares</th><th>Class</th><th>Vesting</th></tr>
</thead>
<tbody>
<tr><td><code>{{#grants}}</code></td><td></td><td></td><td></td></tr>
<tr><td><input name="grant_date" placeholder="grant_date"></td><td><input name="shares" placeholder="shares"></td><td><input name="class_of_stock" placeholder="class_of_stock"></td><td><input name="vesting_schedule" placeholder="vesting_schedule"></td></tr>
<tr><td><code>{{/grants}}</code></td><td></td><td></td><td></td></tr>
</tbody>
</table>
<p>Tax treatment is your responsibility; see also <a href="#sec-comp">your cash compensation 1.(a)</a>.</p>
<h2 id="sec-conting"><span class="heading-number">(c)</span> Contingencies</h2>
<p>This offer is contingent on each of the following:</p>
<code>{{#contingencies}}</code><ul>
<li>
<input name="." placeholder="."></li>
</ul>
<code>{{/contingencies}}</code><h2 id="sec-atwill"><span class="heading-number">(d)</span> At-Will Employment</h2>
<p>Your employment with <input name="company_common_name" placeholder="company_common_name"> is at will: either you or the Company may end it at any time, with or without cause. Nothing in <a href="#sec-comp">Section 1.(a)</a> or <a href="#sec-equity">Section 1.(b)</a> changes that.</p>
<hr>
<p>To accept, sign below by <input name="offer_expiration_date" placeholder="offer_expiration_date">.</p>
Expand All @@ -17,7 +34,7 @@ <h2 id="sec-atwill"><span class="heading-number">(c)</span> At-Will Employment</
<tr><td>Name: <input name="hiring_manager_name" placeholder="hiring_manager_name"></td><td>Name: <input name="candidate_name" placeholder="candidate_name"></td></tr>
<tr><td>Date: <input name="offer_date" placeholder="offer_date"></td><td>Date: <input name="signature_date" placeholder="signature_date"></td></tr>
</tbody></table>
<p>Please retain a copy of this letter for your records; the terms in <span>Sections <a href="#sec-comp">1.(a)</a>, <a href="#sec-equity">1.(b)</a> and <a href="#sec-atwill">1.(c)</a></span> are the entire agreement.</p>
<p>Please retain a copy of this letter for your records; the terms in <span>Sections <a href="#sec-comp">1.(a)</a>, <a href="#sec-equity">1.(b)</a> and <a href="#sec-atwill">1.(d)</a></span> are the entire agreement.</p>
<section class="footnotes" role="doc-endnotes">
<ol>
<li id="fn-1">
Expand Down
22 changes: 22 additions & 0 deletions examples/legal_demo.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,31 @@
"Subject to Board approval, you will be granted the right to purchase {{number_shares}} shares of {{class_of_stock}} under a Restricted Stock Purchase Agreement, vesting over {{vesting_schedule}}.\n",
"{{/equity.restricted_stock}}\n",
"\n",
"Your grants under this offer are:\n",
"\n",
"| Grant date | Shares | Class | Vesting |\n",
"|---|---|---|---|\n",
"{{#grants}}\n",
"| {{grant_date}} | {{shares}} | {{class_of_stock}} | {{vesting_schedule}} |\n",
"{{/grants}}\n",
"\n",
"Tax treatment is your responsibility; see also [your cash compensation @sec-comp]."
]
},
{
"cell_type": "markdown",
"id": "8647cece",
"metadata": {},
"source": [
"## Contingencies {#sec-conting}\n",
"\n",
"This offer is contingent on each of the following:\n",
"\n",
"{{#contingencies}}\n",
"- {{.}}\n",
"{{/contingencies}}"
]
},
{
"cell_type": "markdown",
"id": "bb626ab6",
Expand Down
16 changes: 16 additions & 0 deletions examples/legal_demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,24 @@ Subject to approval by {{company_common_name}}'s Board of Directors, you will be
Subject to Board approval, you will be granted the right to purchase {{number_shares}} shares of {{class_of_stock}} under a Restricted Stock Purchase Agreement, vesting over {{vesting_schedule}}.
{{/equity.restricted_stock}}

Your grants under this offer are:

| Grant date | Shares | Class | Vesting |
|---|---|---|---|
{{#grants}}
| {{grant_date}} | {{shares}} | {{class_of_stock}} | {{vesting_schedule}} |
{{/grants}}

Tax treatment is your responsibility; see also [your cash compensation @sec-comp].

## Contingencies {#sec-conting}

This offer is contingent on each of the following:

{{#contingencies}}
- {{.}}
{{/contingencies}}

## At-Will Employment {#sec-atwill}

Your employment with {{company_common_name}} is at will: either you or the Company may end it at any time, with or without cause. Nothing in [@sec-comp] or [@sec-equity] changes that.
Expand Down
Binary file modified examples/legal_demo.pdf
Binary file not shown.
20 changes: 20 additions & 0 deletions examples/legal_demo.typ
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,28 @@ Subject to Board approval, you will be granted the right to purchase #raw("{{num

#raw("{{/equity.restricted_stock}}")

Your grants under this offer are:

#table(
columns: 4,
table.header([Grant date], [Shares], [Class], [Vesting]),
[#raw("{{#grants}}")], [], [], [],
[#raw("{{grant_date}}")], [#raw("{{shares}}")], [#raw("{{class_of_stock}}")], [#raw("{{vesting_schedule}}")],
[#raw("{{/grants}}")], [], [], [],
)

Tax treatment is your responsibility; see also #ref(<sec-comp>, supplement: [your cash compensation]).

== Contingencies <sec-conting>

This offer is contingent on each of the following:

#raw("{{#contingencies}}")

- #raw("{{.}}")

#raw("{{/contingencies}}")

== At-Will Employment <sec-atwill>

Your employment with #raw("{{company_common_name}}") is at will: either you or the Company may end it at any time, with or without cause. Nothing in #ref(<sec-comp>, supplement: [Section]) or #ref(<sec-equity>, supplement: [Section]) changes that.
Expand Down
8 changes: 4 additions & 4 deletions examples/render_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ def _tok(n, h):


def _control(body, syntax, form):
"Interactive form register: variables become click-and-type content controls, section markers stay literal"
if mustache_kind(body) == 'section': return '{{' + body + '}}'
"Interactive form register: variables become click-and-type content controls, section markers and the `{{.}}` item placeholder stay literal"
if mustache_kind(body) == 'section' or body.strip() == '.': return '{{' + body + '}}'
return 'control', body

convert(to_mdhtml(src, templates=MUSTACHE), d/'legal_demo-form.docx', tmpl=_control, number_headings='legal')


def _bound(body, syntax, form):
"Synced form register: every control for a variable is a live view of one shared XML node"
if mustache_kind(body) == 'section': return '{{' + body + '}}'
"Synced form register: every control for a variable is a live view of one shared XML node; the `{{.}}` item placeholder has no named node to bind to, so it stays literal"
if mustache_kind(body) == 'section' or body.strip() == '.': return '{{' + body + '}}'
return 'bound', body

convert(to_mdhtml(src, templates=MUSTACHE), d/'legal_demo-bound.docx', tmpl=_bound, number_headings='legal')
Expand Down
Loading