Skip to content
Merged
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
13 changes: 10 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,22 @@ jobs:

# The claims the pages make in prose — the counts, the completeness, the rules —
# which a table cannot carry and a name cannot say.
- name: The claims the pages make
# The claims the pages make, and the client the skill carries: the client is a
# program that talks to an application over a socket, and nothing else would say
# whether it asks what it should.
- name: The claims the pages make, and the client the skill carries
working-directory: documentation
run: vendor/bin/pest

# A site whose links 404 is worse than no site, and nothing else says so: the build
# succeeds, the pages are there, and the reader is the one who finds out.
- name: The site's own links
#
# The skill is checked here too, because it is built into the site: a bundle that has
# fallen behind the pages it was made from is a bundle that is quietly wrong, and
# nothing else would say so.
- name: The site's own links, and the skill it publishes
working-directory: documentation
run: php tools/check-site-links.php
run: npm run check:site

# The pages under `content/command-line/` are written by hand, and nothing else says
# whether they still describe the program. A command that is implemented and not
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ Those are the site's own routes. The site is served at the root of its host, so
files carry no path in front of them: the addresses in `llms.txt` and `pages.json` are
absolute, and the links inside the Markdown mirrors are the routes as they are written.

## The skill

The site also publishes a **skill**: `/skills/mdword.zip`, and `/skills/mdword/` file by
file, with `/skills/index.json` as the catalogue OpenCode reads over the network. It is the
documentation above as a directory an agent can read, with the examples beside it and the
tools that render a document so that it can be looked at.

**It is built by the same build**, out of the same sources, so it cannot say anything the site
does not say. `tools/check-skill.php` is what holds it to that: every reference file must be
the page it carries, every page of the sections it covers must be carried, and the scripts
must be the ones the library ships.

## Where it is published

`.github/workflows/pages.yml` builds the site and publishes it to
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"@php tools/check-readme-links.php ../markdown-word ../markdown-presentation ../mdoffice",
"@test"
],
"check:site": "@php tools/check-site-links.php",
"check:site": ["@php tools/check-site-links.php", "@php tools/check-skill.php"],
"check:links": "@php tools/check-readme-links.php ../markdown-word ../markdown-presentation ../mdoffice"
},
"minimum-stability": "dev",
Expand Down
100 changes: 100 additions & 0 deletions content/skills/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
layout: layouts/base.njk
permalink: /skills/
title: The skill
summary: A directory an agent reads, holding this site's documentation and the library's examples — and where each agent harness looks for one.
section: The skill
order: 0
---

An agent asked to turn Markdown into a Word document has to know four things: what the
library can do, how to build a document with it, how to *look* at what it built, and how to
use a design that already exists. This page is how it gets them.

## What it is

**A skill is a directory.** It holds a `SKILL.md` — a short account of what it is for and
when to use it — and beside it the files it points at: the documentation, the examples, and
the scripts that render a document so that it can be looked at.

**The one for this suite is `mdword`**, and it is built from this site. The pages in it are
the pages of this site; the examples in it are this site's own and the library's. Nothing in
it is written twice: it is a copy made by the build, and a copy that is made by the build
cannot drift from what it was made from.

## Install it

OpenCode reads it over the network and needs nothing downloaded — one line in
`opencode.json`:

```jsonc
{
"$schema": "https://opencode.ai/config.json",
"skills": ["https://markdown-office.github.io/skills/"]
}
```

Every other agent reads a directory, and the whole bundle is one file:

```bash
curl -LO https://markdown-office.github.io/skills/mdword.zip
unzip -q mdword.zip -d ~/.agents/skills/mdword
```

## Where it goes

| Agent | It looks here | What to do |
| --- | --- | --- |
| **Codex** | `~/.agents/skills/mdword` | nothing — that is where it is |
| **OpenCode** | `~/.agents/skills/mdword`, `~/.config/opencode/skills/mdword` and `~/.claude/skills/mdword` | nothing, or the catalog above |
| **Claude Code** | `~/.claude/skills/mdword` | a symlink to the unpacked copy |
| **GitHub Copilot** | `~/.copilot/skills/mdword`, and `.github/skills/mdword` in a repository | a symlink to the unpacked copy |

So the bundle is unpacked in one place and the other two are links to it:

```bash
ln -s ~/.agents/skills/mdword ~/.claude/skills/mdword
ln -s ~/.agents/skills/mdword ~/.copilot/skills/mdword
```

A repository can carry one of its own instead, under `.agents/skills/` or `.github/skills/`,
and it is found from the repository root rather than from the machine.

## Is the copy current?

The version is in `VERSION` beside the `SKILL.md`, and it is the same string as the `version`
in the published catalogue. A copy whose `VERSION` differs from the catalogue's is a copy
from before, and the catalogue is the newer of the two:

```bash
curl -s https://markdown-office.github.io/skills/index.json
```

## What is in it

```
mdword/
├── SKILL.md what the library does, and how to use it
├── VERSION the version of this bundle
├── references/ the documentation, one file per page
├── examples/ this site's examples, and the library's
│ ├── site/
│ └── library/
└── scripts/ the render tools
```

The **references** are the pages of this site, one file each, with the frontmatter off. The
**examples** are the decks and documents the pages show, built and named, and the library's
own twenty-three — which between them are the catalogue of what the library can do.

The **scripts** render a document so that it can be looked at, which is the one step a
prose answer cannot do for itself.

## What it is not

**It does not replace this documentation.** It is a copy of it, made by the same build that
makes the site, and the site is where a change is made first. A copy of the documentation is
not the documentation: it is a copy of it, as of the moment the copy was made.

And it is **not a second implementation of anything**. The library is the library; the skill
says how to drive it.
2 changes: 1 addition & 1 deletion content/word/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ permalink: /word/errors/
title: Errors
summary: Every exception Markdown Word throws, and what each one means.
section: Markdown Word
order: 5
order: 6
---

Every exception Markdown Word throws, in one place. The one to catch, and the advice that is
Expand Down
2 changes: 1 addition & 1 deletion content/word/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ permalink: /word/images/
title: Image options
summary: The three options that decide what happens to a picture, and the two calls that report what happened to it.
section: Markdown Word
order: 4
order: 5
---

Word holds a picture one of three ways, and the option says which. The formats themselves — what
Expand Down
10 changes: 9 additions & 1 deletion content/word/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,10 +281,18 @@ try {

The whole catalogue is on [Errors](/word/errors/).

### For an agent

There is a **skill**: a directory an agent reads, holding this half's documentation, the
library's examples, and the tools that render a document so that it can be looked at. It is
built from this site, so it is never out of step with it.

[How to install it](/skills/).

---

## Next

[Markdown Presentation](/presentation/) · [The directive syntax](/directives/syntax/) ·
[Word directives](/word/directives/) · [Images](/images/) ·
[Round trips](/word/round-trip/) · [Errors](/word/errors/)
[Round trips](/word/round-trip/) · [Errors](/word/errors/) · [The skill](/skills/)
2 changes: 1 addition & 1 deletion content/word/round-trip.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ permalink: /word/round-trip/
title: Round trips
summary: Markdown to a document and back. What comes out the same, and the six things that do not.
section: Markdown Word
order: 3
order: 4
---

Both directions are the same interface, so a round trip is two calls:
Expand Down
170 changes: 170 additions & 0 deletions content/word/templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
---
layout: layouts/base.njk
permalink: /word/templates/
title: Templates
summary: Rendering into a design that already exists — a Word template with its own styles, and a theme for one that is not a Word document.
section: Markdown Word
order: 3
---

## Two ways to bring a design

There are two, and they answer different questions.

| | The design is | You give it | It gives you |
| --- | --- | --- | --- |
| **A template** | a Word document — a `.docx` or a `.dotx` | the `styleId`s of its styles | its fonts, colours, spacing, headers and footers, and its letterhead |
| **A theme** | a directory of YAML | nothing | a look of your own, in every document that names it |

A template is a design somebody made in Word. A theme is a design you write down, and it is
the only one of the two that travels with the Markdown rather than with the document.

## A Word template

Point the renderer at the **styleIds** your template defines and it inherits the whole design
— fonts, colours, spacing, headers and footers.

```php
use MarkdownWord\Configuration;
use MarkdownWord\Configuration\Styles;
use MarkdownWord\Template\MarkdownTemplate;

$config = Configuration::create()->withStyles([
Styles::HEADING_1 => 'ReportTitle', // a styleId in your template
Styles::PARAGRAPH => 'BodyText',
Styles::BLOCK_QUOTE => 'PullQuote',
]);

(new MarkdownTemplate('report-template.docx', $config))
->insert(file_get_contents('summary.md'))
->save('northwind.docx');
```

A `.docx` and a `.dotx` are both accepted, and which one it is makes no difference to the
result: **a filled-in template is always a document.** See [What a `.dotx` costs](#what-a-dotx-costs)
for why that is worth saying.

Everything a template says is something Word already understands, because a template comes from
Word and anything else would have to be invented here:

| | |
| --- | --- |
| A value | a **`MERGEFIELD`**, the field a person inserts with `Ctrl+F9` |
| A place for the content | a **rich-text content control**, named `body` |

There is no third thing. A row of records is a table in the Markdown, and the template styles
it like any other content — which is why there is no loop to explain.

### The values

`fields()` fills the template's `MERGEFIELD`s, and the document carries its own values so
that the document and everything in it travel together:

```php
(new MarkdownTemplate('report-template.docx'))
->fields(['customer' => 'Northwind Ltd'])
->insert(file_get_contents('summary.md'))
->save('northwind.docx');
```

**Filled fields are unlinked.** The value stays and the field goes, which is what Word's own
*Finish & Merge → Edit Individual Documents* produces and what a finished document wants:
nothing to refresh with `F9`, and a value that can hold `&` or `<` without becoming an
instruction.

### The place for the content

**`body` is this library's convention, not Word's.** ECMA-376 §17.5.2.36 defines a content
control's `w:tag` as "an arbitrary string which applications can associate with a structured
document tag", reserves no value for it, and Word itself never reads one — it round-trips it.
So the name collides with nothing Word recognises, and no standard is being missed by not
matching one.

The control is matched on its **tag**, which is the field name Word gives it, rather than on
its alias: the alias is the label a person sees in Word's own UI and is the one they will
rename while editing the template, whereas the tag is what a program reads. A template that
has both is written by something that knows the difference.

A template that names its control something else is asked for that name:

```php
(new MarkdownTemplate('letterhead.docx'))->insert($markdown, 'letter');
```

Nobody else does this: the templating libraries that exist either use plain text delimiters
(`{name}` in docxtemplater, `{{ var }}` in python-docx-template) or invent their own prefix,
and none of them claims `body`.

### A template with no control

**A template need not be one this library's generator wrote.** A letterhead a company made in
Word to be filled in by a person has fields and no control, and refusing those would leave
the library able to use only the templates it wrote itself.

So a template with no control of that name takes the content **at the end of its body**, or
says so and refuses. Which one is the `contentFallback()` setting — `ContentFallback::Append`
or `ContentFallback::Error` — and the refusal is `--require-control` on the command line.

Filling the fields is independent of it: a template can have values and nowhere to put the
prose.

## The same thing from the frontmatter

A document may name its own template, and then nothing has to be supplied from PHP to
reproduce it:

```yaml
---
template_file: ../templates/report.dotx
fields:
customer: Northwind Ltd
---
```

`template_file:` names a `.docx` or a `.dotx`, and the path is resolved against the
document's own directory. `fields:` fills the template's `MERGEFIELD`s, with `true` and
`false` spelled as strings because that is what Word stores.

The two routes are the same thing reached two ways, and the difference is where the styleIds
come from: the frontmatter route cannot name them, so it inherits the template's own styles
rather than mapping this library's slots onto them.

## What a `.dotx` costs

Nothing to use one, and one thing to *produce* from one: the result is a document, not a
filled template. That is the only difference between the two, and it is worth saying because
the alternative is worse.

A `.dotx` and a `.docx` are the same archive with one line of `[Content_Types].xml` different,
and that line is what Word decides a file by. A package typed as a template is opened by
*creating a new document from it* — double-clicking a filled-in `.dotx` gives you
`Document1.docx` rather than the file you were sent, and anything dispatching on MIME type
treats it as a template. So rendering into one writes a document, and says so by writing one:

```php
$template = new MarkdownTemplate('report-template.dotx');
$template->insert('body', $markdown)->save('northwind.docx'); // a .docx, whatever went in
```

## A theme

A theme is a directory: `theme.yml` — `name` required, plus `description` and `author` — and
an optional `styles.yml` holding the same slot names as
[the configuration](/word/configuration/#every-style-slot). One key of that file is not a slot:
`textStyles` holds the named styles a document may write a word in.

A document names one with `theme:`, and the ones that ship are listed with the configuration, on
[the themes](/word/configuration/#themes).

A theme may also name the `theme.yml` itself, in which case the `styles.yml` beside it is the
theme's, and a document that names one by path gets that one.

## The command line

```bash
mdoffice word to-docx notes.md --template report.dotx --require-control
```

`--template` takes a `.docx` or a `.dotx`, and `--require-control` refuses a template with no
content control instead of appending to it. The rest of the options are on
[For word](/command-line/word/).
Loading
Loading