Decompiling an Excel template into a transform - #3 XSLT
This is the XML counterpart to the C# and F# walkthroughs, but the pitch is different: there’s no C# or F# code in this one at all. If your data already comes out of a pipeline as XML — an XSLT transform, a database export, whatever — you can go straight from that data to a real Excel file, using the same template-driven idea as the other demos, but with a declarative transform instead of a programming language. The full project is on GitHub: Kookerella.Demo.DecompileToSource.
What we’re starting from
The same plain Excel file used in the other demos: a bold, 16pt “INVOICE” title, a
white-on-navy header row with a bottom border, and two sample line-item rows with an
Amount column computed as Qty * Unit Price.
Step 1: Install the tool
dotnet tool install -g Kookerella.FsOpenXmlDsl.Mcp
Step 2: Reverse-engineer the template
fsopenxmldsl-mcp convert templates/InvoiceTemplate.xlsx --lang xml -o decompiled/InvoiceTemplate.xml
This prints out plain XML — not source code in any programming language, just the
workbook’s own data and styling, validated against the tool’s own Xml.xsd schema. A
trimmed excerpt, the “Widgets” row:
<cell ref="A4">
<text>Widgets</text>
</cell>
<cell ref="B4">
<number>3</number>
</cell>
<cell ref="C4">
<number>9.99</number>
<style>
<numberFormat kind="currency" />
</style>
</cell>
<cell ref="D4">
<formula cachedValue="29.97">B4*C4</formula>
<style>
<numberFormat kind="currency" />
</style>
</cell>
Text, numbers, a formula with its cached value, and styling — all as plain elements and attributes, no code to read.
Step 3: Turning the decompiled snippet into a transform
The decompiled XML is disposable — it’s the starting point, not the deliverable. The
maintained version is split across two files: orders.xml (the data that varies invoice
to invoice, in whatever shape your own pipeline already produces it), and invoice.xslt
(the styling, copied verbatim from the decompiled XML, with the two hardcoded item rows
replaced by an xsl:for-each).
The diff from the decompiled file is deliberately small — the title cell and the four header cells are copied byte-for-byte. Only the two hardcoded “Widgets”/“Gadgets” cell blocks became this:
<xsl:for-each select="order">
<xsl:variable name="row" select="3 + position()" />
<xsl:variable name="amount" select="xs:double(@quantity) * xs:double(@unitPrice)" />
<cell ref="{concat('A', $row)}">
<text><xsl:value-of select="@item" /></text>
</cell>
<cell ref="{concat('B', $row)}">
<number><xsl:value-of select="@quantity" /></number>
</cell>
<cell ref="{concat('C', $row)}">
<number><xsl:value-of select="@unitPrice" /></number>
<style>
<numberFormat kind="currency" />
</style>
</cell>
<cell ref="{concat('D', $row)}">
<formula cachedValue="{$amount}"><xsl:value-of select="concat('B', $row, '*C', $row)" /></formula>
<style>
<numberFormat kind="currency" />
</style>
</cell>
</xsl:for-each>
This is the XSLT mirror of the C# demo’s collection-expression spread and the F# demo’s
yield! — same idea (splice a computed sequence of rows into a template), a third
different syntax for it.
Step 4: Running the transform
XSLT needs an XSLT processor — .NET doesn’t ship one built in for XSLT 3.0, so this demo
uses Saxon HE, distributed as a
dotnet local tool:
dotnet tool restore
dotnet SaxonHE12NetXslt -s:orders.xml -xsl:invoice.xslt -o:invoice-workbook.xml
That produces invoice-workbook.xml — the same shape as the decompiled XML, except with
three real data rows instead of two hardcoded ones.
Step 5: Building the Excel file
fsopenxmldsl-mcp build invoice-workbook.xml invoice.xlsx
build is the inverse of convert --lang xml: it reads XML matching the tool’s
Xml.xsd schema and writes a real .xlsx. No C# or F# was compiled or run anywhere in
this pipeline. The only “code” is the .xslt stylesheet itself — a declarative
transform, not a program.
Step 6: Prove it stays correct
Because the schema and fsopenxmldsl-mcp build are real, checkable things, this
pipeline is tested by running the actual commands and inspecting the result, rather than
trusting that the XSLT “looks right”:
- Schema validity — runs a real
OpenXmlValidatorover the output. - Correctness — feeds in 0, 1, and 3 orders and checks the row count and the
Amountformula for each one. - Styling preservation — pins the exact colors, bold, font size, and border style that came from the original template.
Recap
| Step | Command | Output |
|---|---|---|
| Install the tool | dotnet tool install -g Kookerella.FsOpenXmlDsl.Mcp | fsopenxmldsl-mcp on your PATH |
| Reverse-engineer | fsopenxmldsl-mcp convert InvoiceTemplate.xlsx --lang xml -o InvoiceTemplate.xml | Plain XML that reproduces the original .xlsx exactly |
| Make it data-driven | Replace hardcoded <cell> blocks with xsl:for-each over your own data | invoice.xslt |
| Transform | dotnet SaxonHE12NetXslt -s:orders.xml -xsl:invoice.xslt -o:invoice-workbook.xml | Workbook XML with real data rows |
| Build | fsopenxmldsl-mcp build invoice-workbook.xml invoice.xlsx | A real, schema-valid .xlsx |
| Verify | dotnet test | Schema validity, formula correctness, and styling all pinned by real tests |