Law Review DOCX Export

SkillDocs & knowledge

Use this skill to BUILD a formatted Word document from LAW REVIEW / legal MARKDOWN drafts, Bluebook footnotes, TOC, small caps, styled tables and vector figures. Triggers: 'build the law review document', 'make the Word version of my article', 'turn my markdown draft into Word', 'export the article to docx', 'compile/finalize the draft', 'make the submission docx for the law review', 'apply the law review template', 'the figure is blank in Word', 'fix the widows in the compiled PDF'. Use proactively once a legal markdown draft is finished and a Word file is implied but unnamed. NEGATIVE ROUTING: NOT 'law-econ-docx' (author-date citations plus a reference list, for JLE/JLS/JLEO/ALER, different citation model), NOT 'docx-typst' (source is Typst, not markdown), NOT the generic 'docx' skill (edits docx content), NOT 'docx-render' (renders an existing docx to PDF/PNG only), NOT 'docx-repair' (fixes a cloud-editor-damaged docx).

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Law Review DOCX Export skill

What this skill tells your AI

The instructions your AI receives, as published by edwinhu/workflows in skills/law-review-docx/SKILL.md and read by ahel’s review.

What this skill carries — grep references/ for any subject the names below miss: !d=${CLAUDE_SKILL_DIR}; command -v skill-toc >/dev/null 2>&1 && exec skill-toc "$d"; s=$HOME/.claude/skills/plugin-utils/bin/skill-toc; [ -x "$s" ] && exec "$s" "$d"; echo "(skill-toc unavailable: references and scripts are NOT listed here — install the plugin-utils plugin, or start a new session so its bin/ reaches PATH)"

Convert markdown drafts into a properly formatted Word document using the law review template via pandoc.

This is the ONLY correct way to build a law-review .docx — never hand-roll pandoc/soffice; the template, footnote handling, TOC, and table styling all live in build_docx.py. Agents without the Skill tool (most workflow subagents) can't invoke this skill — run the script below directly:

Usage

uv run python3 ${CLAUDE_SKILL_DIR}/scripts/build_docx.py PROJECT_DIR [--output PATH] [--fix-footnotes]

The script:

  1. Detects title/author from .planning/ACTIVE_WORKFLOW.md or PRECIS.md
  2. Combines all drafts/*Draft*.md files in section order (Introduction → Parts → Conclusion → Appendix)
  3. Strips YAML frontmatter and prefixes footnote labels to avoid cross-section collisions
  4. Resolves <!-- include: PATH --> sentinels by inlining file contents (paths must be absolute or ~-expanded)
  5. Runs pandoc with --reference-doc pointing to the law review template
  6. Optionally runs the docx-repair repair script (--fix-footnotes)

Compile-Time Includes

To embed externally generated tables or fragments at build time, place a sentinel in the draft:

<!-- include: ~/projects/mirror/data/tables/paper/table2_body.md -->

The preprocessor expands ~, reads the file, and splices its contents inline before pandoc runs. Missing or non-absolute paths emit a visible <!-- MISSING: ... --> placeholder instead of failing silently. For images, use plain pandoc markdown (![caption](~/path/to/figure.png)) — no sentinel needed.

Detecting the Project Directory

If the user doesn't specify a path, detect it from context:

  1. Check if current working directory has drafts/ and .planning/
  2. Check if .planning/ACTIVE_WORKFLOW.md exists and read project_dir from it
  3. Follow symlinks (e.g., paper → actual project directory)

Template

The reference template lives at:

${CLAUDE_PLUGIN_ROOT}/references/templates/law_review_template.docx

This template defines all styles that pandoc applies:

StyleUseFormatting
TitleArticle titleBold, small caps, centered
Heading 1Part titles (I., II., III.)Bold, left-aligned
Heading 2Sections (A., B., C.)Bold, left-aligned
Heading 3Subsections (1., 2., 3.)Italic, left-aligned
Body TextAll body paragraphsFirst-line indent
First ParagraphAfter headingsNo indent
Footnote TextFootnotes10pt, single-spaced

Figures: vector, via svgBlip

Reference the PNG in markdown — ![caption](~/figures/fig1.png) — and keep a same-stem .svg beside it. After pandoc, build_docx.py finds each embedded raster, matches it to its SVG by content hash (pandoc rewrites media to rIdN.png, so the filename is gone by then), and attaches the vector:

<a:blip r:embed="rIdPng">
  <a:extLst>
    <a:ext uri="{96DAC541-7B7A-43D3-8B79-37D633B846F1}">
      <asvg:svgBlip xmlns:asvg="http://schemas.microsoft.com/office/drawing/2016/SVG/main"
                    r:embed="rIdSvg"/>
    </a:ext>
  </a:extLst>
</a:blip>

Word 2016+ draws the SVG with its own renderer; everything older falls back to the PNG. No sibling .svg, no change — raster-only projects are unaffected.

Never reference a bare .svg from markdown. Pandoc embeds it as an image part with no svgBlip, unzip -l shows the media happily, and Word renders nothing — blank space under the caption, no error at any stage.

Never convert SVG→EMF with LibreOffice as a substitute. EMF is a real vector format and Word draws it, so the route looks correct. It is not: LibreOffice's SVG importer silently corrupts complex figures. A five-facet histogram came back missing an entire facet row, every row label, both axis labels, the tick numbers and the zero line — still a plausible-looking chart, so nothing downstream flagged it. Simple one-panel figures convert fine, which is what makes it dangerous: verifying one figure proves nothing about the rest.

Verification — count images in the RENDERED PDF, never in the DOCX:

pdfimages -list manuscript.pdf | tail -n +3 | wc -l   # 0 == every figure is vector

A media part exists for formats Word cannot draw, so a word/media/ count is not evidence the figure reached the page. Check the most structurally complex figure against its source, not the first one.

After Export

Report the output path, section count, footnote count, and approximate word count. If the user needs further formatting (NOTEREF cross-references, footnote repair from cloud editing), suggest --fix-footnotes or the docx-repair skill.

Typographic Widows

build_docx.py sets Word's paragraph-level widowControl, which prevents a paragraph's last line from landing alone on the next page — it does nothing about a last line holding one or two stray words. Two companion scripts handle that, after compiling to PDF:

# Report widows (paragraph last lines of 1-2 short words, main column only)
uv run python3 ${CLAUDE_SKILL_DIR}/scripts/check_widows.py OUTPUT.pdf [--max-words N] [--verbose]

# Same detection, then bind the last two words in the offending drafts/*.md
# paragraph with a pandoc non-breaking space. Recompile afterwards.
uv run python3 ${CLAUDE_SKILL_DIR}/scripts/fix_widows.py OUTPUT.pdf PROJECT_DIR [--dry-run]

Run --dry-run first: fix_widows.py edits source markdown, and the fix is only meaningful against the PDF it was measured on. Iterate compile → check → fix → recompile.

Rendering to PDF (Word fidelity, incl. from background jobs)

build_docx.convert_to_pdf() delegates to doc_render.convert(renderer="word"), which uses Microsoft Word's engine for line-exact layout (best for widow detection) and faithful tables.

Note on table fidelity: for tables this skill builds, the wrap_cell pass (see style_tables) already pre-breaks cells with explicit <w:br/> so soffice and x2t render the grid too (commit ec349c5), and x2t kerning is corrected by doc_render's GPOS/kern injection + EB-Garamond substitution. So all three engines are grid-faithful for build-generated tables; Word is preferred for polish, not required for table integrity. Word matters most for hand-authored docx whose tables never pass through wrap_cell — LibreOffice collapses such a table to a single stacked column whenever a cell must auto-wrap (Word/x2t keep the grid).

Word is GUI-driven, so a detached Claude background job can't drive it directly (AppleEvents fail with -600 — it's in a non-console GUI session without Word's TCC grant). doc_render handles this transparently by dispatching the render into a cmux pane (which lives in the console GUI session and is TCC-granted). One-time host prerequisites:

  • cmux socket control enabled: automation.socketControlMode"cmuxOnly" in ~/.config/cmux/cmux.json, then cmux reload-config.
  • Microsoft Word granted to cmux under System Settings → Privacy & Security → Automation, and Word in Full Disk Access.

Set $DOC_RENDER_NO_CMUX=1 to disable the cmux path (then background jobs fall back to x2t/LibreOffice). See docs/investigations/2026-06-22_word-render-cmux-dispatch.md.

Known Gotcha: Pandoc-Citeproc Paren-Wrap Inside Footnotes

Symptom. In the compiled DOCX, some footnotes read with a doubled space and wrapping parens around a citation:

see  (Griffin, supra note 12; Macey, supra note 12). For proponents...

(note the two spaces before ().

Root cause. Pandoc-citeproc wraps any bracketed parenthetical citation [@key] or [signal @key] in parens with a leading space when it appears mid-paragraph inside a footnote body. At the paragraph start the wrap is suppressed; mid-paragraph it is not. This is native pandoc behavior for note-style CSLs and cannot be fixed at the CSL level.

Why the natural-looking fix doesn't work. Rewriting source to bare textual form (@key without brackets) renders cleanly only if every citation has a locator. For bib entries without locators (books, misc, many articles), pandoc-citeproc with a note-style CSL emits just a stray number (1.) because the full cite is supposed to go into a footnote and there is no footnote to host it (we're already inside one).

Fix. The docx-repair skill's fix_footnotes.py detects and strips these wraps post-compile. The detector keys on the distinctive XML signature:

<w:r><w:t xml:space="preserve"> </w:t></w:r>     <!-- natural space -->
<w:r><w:t xml:space="preserve"> </w:t></w:r>     <!-- EXTRA space -->
<w:r>…<w:t>(Author,</w:t></w:r>                  <!-- open paren run -->
… citation content …
<w:r>…<w:t>)</w:t></w:r>                         <!-- close paren (standalone or attached) -->

Author-written explanatory parentheticals ((describing X), (documenting Y)) appear as a single <w:t> (…)</w:t> run and lack the double-whitespace signature, so they are preserved. build_docx.py runs fix_footnotes.py automatically when --fix-footnotes is set (the default).

Title-page spacing

Front matter is usually unstyled Normal, which defines no spacing, so the title page's rhythm rides on manual empty paragraphs. Measured norms from six published typesets, which one to copy (Griffin — the manuscript-laid-out one, uniform 2.0×, not the ~4× journal-page average), and the page-one fit trap: references/title-page-spacing.md.

Red Flags

ActionWhy WrongDo Instead
Running pandoc -o output.docx without --reference-docProduces default Calibri formatting that violates journal requirementsAlways use the template
Manually constructing the DOCX with python-docx or docx-jsReinvents what the template + pandoc already handleRun the script
Combining markdown without prefixing footnote labelsCauses footnote collisions when multiple sections use [^1]The script handles this automatically
Referencing a bare .svg from markdownPandoc writes no svgBlip, so Word renders nothing — blank space under the caption, no errorReference the PNG; keep the .svg beside it and let the build attach it
Converting SVG→EMF with LibreOffice to get vector into WordIts SVG importer silently drops facet rows, row labels, axis labels and reference lines from complex figuresUse the svgBlip path the build already implements
Confirming figures embedded by counting word/media/ entriesA media part exists for formats Word cannot drawpdfimages -list the rendered PDF
Declaring the figure pipeline verified after checking one figureConverters that mangle complex figures handle simple ones fineVerify the most structurally complex figure

Signals

GitHub stars
21
Forks
4
Last commit
Sep 2026

ahel review

  • K6low
    bundled executables the agent is told to run

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
law-review-docx
Source
github.com/edwinhu/workflows