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
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@ file. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning
follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- Word's multilevel numbering through HTML. The reader counts lists on the
way Word does — a list broken off by a paragraph continues (`<ol start>`),
a level below hangs under its parent's number through a label-less parent
item (`ListItem::$markerHidden`) — and keeps each level's label pattern
(`ListNode::$levelText`: `%1.%2.`, `4.%1.`). The serializer writes the
labels as `::marker` rules on per-level classes into
`ImportedDocument::$css`; the converter reads them back from `<style>`
(`Converter::markerStyleOf()` carries them past a CSS inliner), and the
writer emits such a list as one Word multilevel numbering. A job
description numbered `3.1 … 3.21` came out as `1 … 21` under every
heading.
- `Footnote::$sizePt`: the size a note is set in (from its runs in
`footnotes.xml`), written as `font-size` on `span.footnote` — notes are
usually 7–10 pt, and at the body's size a note took lines the page does
not have.
- Each level keeps where Word places it (`ListNode::$indentLeftTwips`, from
the numbering's `w:ind`): the serializer writes the list without the
browser's nesting indent and moves every item by what its level adds.

### Fixed
- Reading headers and footers took the references of every section, and a
later section's part won: a seven-page form came out with the footer of
its last section and the first-page footer of its first. The first
section's parts are read now — they govern the document's first pages —
and first-page parts only with `<w:titlePg/>`, even-page ones only with
`<w:evenAndOddHeaders/>`, as Word shows them.
- `numbering.xml` interleaved custom `abstractNum` and `num` elements; the
schema wants every `abstractNum` first.
- A table cell was read paragraph by paragraph, and its numbered and
bulleted items came out as plain paragraphs without markers. A cell is now
read like a body: lists, nested tables and all.

## [1.14.1] — 2026-08-17

### Changed
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ CSS-inliner upstream if needed.
| Links | `<a href>` external, `<a href="#anchor">` internal, `<a id>` bookmarks |
| Images | `<img src="data:image/...;base64,...">` |
| Tables | `<table>`, `<thead>/<tbody>`, `<tr>`, `<th>/<td>`, `<colgroup>/<col>`, `<caption>`, `colspan`, `rowspan` |
| Lists | `<ul>`, `<ol type="a/A/i/I" start="N">`, `<li value="N">`, `<dl>/<dt>/<dd>` |
| Lists | `<ul>`, `<ol type="a/A/i/I" start="N">`, `<li value="N">`, `<dl>/<dt>/<dd>`; Word numbering labels from `.class::marker { content: … counter(list-item) / counters(list-item, ".") … }` in a `<style>`; `<li style="list-style-type:none">` holding only a nested list is a label-less parent (`3.` → `3.1.`) |
| Custom tags | `<page-number/>`, `<page-total/>`, `<current-date format="...">`, `<page-break>` |
| Marker classes | `class="page-number"`, `class="page-total"`, `class="page-break"` — same fields, spelled so an HTML sanitizer keeps them |
| Layout | `<hr>`, `<br>`, `<figure>/<figcaption>` |
Expand Down Expand Up @@ -530,10 +530,16 @@ $imported->watermarkText; // ?string
$imported->pageSettings; // PageSetup
$imported->variables; // list<DetectedVariable>
$imported->media; // array<filename, bytes>
$imported->css; // string — ::marker rules of Word's numbering labels
```

HTML output uses inline styles only — re-loadable into the same library
via `Html\Converter::fromHtml($imported->bodyHtml)`.
HTML output uses inline styles, plus `css` for the labels of Word's
multilevel numbering (`3.1.`, `4.1.`): `::marker` rules on the classes of
the list items, with `<ol start>` where a list broken off by a paragraph
numbers on. Re-loadable into the same library via
`Html\Converter::fromHtml('<style>'.$imported->css.'</style>'.$imported->bodyHtml)`;
an application that inlines CSS first keeps the labels with
`Converter::markerStyleOf($html)`.

---

Expand Down
13 changes: 9 additions & 4 deletions docs/de/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Das Eingabe-HTML darf **nur Inline-Styles** verwenden (keine
| Links | `<a href>` extern, `<a href="#anchor">` intern, `<a id>` Lesezeichen |
| Bilder | `<img src="data:image/...;base64,...">` |
| Tabellen | `<table>`, `<thead>/<tbody>`, `<tr>`, `<th>/<td>`, `<colgroup>/<col>`, `<caption>`, `colspan`, `rowspan` |
| Listen | `<ul>`, `<ol type="a/A/i/I" start="N">`, `<li value="N">`, `<dl>/<dt>/<dd>` |
| Listen | `<ul>`, `<ol type="a/A/i/I" start="N">`, `<li value="N">`, `<dl>/<dt>/<dd>`; Nummerierungsbeschriftungen von Word aus `.class::marker { content: … counter(list-item) / counters(list-item, ".") … }` in einem `<style>`; ein `<li style="list-style-type:none">` nur mit einer verschachtelten Liste ist ein Elternteil ohne Beschriftung (`3.` → `3.1.`) |
| Eigene Tags | `<page-number/>`, `<page-total/>`, `<current-date format="...">`, `<page-break>` |
| Layout | `<hr>`, `<br>`, `<figure>/<figcaption>` |

Expand Down Expand Up @@ -452,11 +452,16 @@ $imported->watermarkText;
$imported->pageSettings;
$imported->variables;
$imported->media;
$imported->css;
```

Die HTML-Ausgabe verwendet nur Inline-Styles und ist über
`Html\Converter::fromHtml($imported->bodyHtml)` wieder in dieselbe
Bibliothek ladbar.
Die HTML-Ausgabe verwendet Inline-Styles und `css` für die Beschriftungen
der mehrstufigen Nummerierung von Word (`3.1.`, `4.1.`): `::marker`-Regeln
auf den Klassen der Listeneinträge, dazu `<ol start>`, wo eine durch einen
Absatz unterbrochene Liste weiterzählt. Wieder ladbar über
`Html\Converter::fromHtml('<style>'.$imported->css.'</style>'.$imported->bodyHtml)`;
eine Anwendung, die CSS vorher inline setzt, behält die Beschriftungen mit
`Converter::markerStyleOf($html)`.

---

Expand Down
12 changes: 9 additions & 3 deletions docs/ru/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ $imported->media;
| Ссылки | `<a href>` внешние, `<a href="#anchor">` внутренние, `<a id>` bookmarks |
| Картинки | `<img src="data:image/...;base64,...">` |
| Таблицы | `<table>`, `<thead>/<tbody>`, `<tr>`, `<th>/<td>`, `<colgroup>/<col>`, `<caption>`, `colspan`, `rowspan` |
| Списки | `<ul>`, `<ol type="a/A/i/I" start="N">`, `<li value="N">`, `<dl>/<dt>/<dd>` |
| Списки | `<ul>`, `<ol type="a/A/i/I" start="N">`, `<li value="N">`, `<dl>/<dt>/<dd>`; метки нумерации Word из `.class::marker { content: … counter(list-item) / counters(list-item, ".") … }` в `<style>`; `<li style="list-style-type:none">` только со вложенным списком — родитель без метки (`3.` → `3.1.`) |
| Custom-теги | `<page-number/>`, `<page-total/>`, `<current-date format="...">`, `<page-break>` |
| Layout | `<hr>`, `<br>`, `<figure>/<figcaption>` |

Expand Down Expand Up @@ -446,10 +446,16 @@ $imported->watermarkText;
$imported->pageSettings;
$imported->variables;
$imported->media;
$imported->css;
```

HTML-output использует только inline-стили — можно подгрузить обратно
через `Html\Converter::fromHtml($imported->bodyHtml)`.
HTML-output использует inline-стили и `css` для меток многоуровневой
нумерации Word (`3.1.`, `4.1.`): правила `::marker` на классах пунктов
списка и `<ol start>` там, где прерванный абзацем список продолжает счёт.
Подгрузить обратно —
`Html\Converter::fromHtml('<style>'.$imported->css.'</style>'.$imported->bodyHtml)`;
приложение, которое сначала встраивает CSS, сохраняет метки через
`Converter::markerStyleOf($html)`.

---

Expand Down
11 changes: 8 additions & 3 deletions docs/zh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ $imported->media;
| 链接 | `<a href>` 外部,`<a href="#anchor">` 内部,`<a id>` 书签 |
| 图片 | `<img src="data:image/...;base64,...">` |
| 表格 | `<table>`、`<thead>/<tbody>`、`<tr>`、`<th>/<td>`、`<colgroup>/<col>`、`<caption>`、`colspan`、`rowspan` |
| 列表 | `<ul>`、`<ol type="a/A/i/I" start="N">`、`<li value="N">`、`<dl>/<dt>/<dd>` |
| 列表 | `<ul>`、`<ol type="a/A/i/I" start="N">`、`<li value="N">`、`<dl>/<dt>/<dd>`;`<style>` 中 `.class::marker { content: … counter(list-item) / counters(list-item, ".") … }` 形式的 Word 编号标签;仅包含嵌套列表的 `<li style="list-style-type:none">` 是无标签的父项(`3.` → `3.1.`) |
| 自定义标签 | `<page-number/>`、`<page-total/>`、`<current-date format="...">`、`<page-break>` |
| 布局 | `<hr>`、`<br>`、`<figure>/<figcaption>` |

Expand Down Expand Up @@ -441,10 +441,15 @@ $imported->watermarkText;
$imported->pageSettings;
$imported->variables;
$imported->media;
$imported->css;
```

HTML 输出仅使用 inline 样式,可通过
`Html\Converter::fromHtml($imported->bodyHtml)` 重新加载到同一个库。
HTML 输出使用 inline 样式,并通过 `css` 提供 Word 多级编号的标签(`3.1.`、
`4.1.`):列表项类上的 `::marker` 规则,以及被段落打断的列表继续计数时的
`<ol start>`。可通过
`Html\Converter::fromHtml('<style>'.$imported->css.'</style>'.$imported->bodyHtml)`
重新加载到同一个库;先内联 CSS 的应用可用 `Converter::markerStyleOf($html)`
保留这些标签。

---

Expand Down
14 changes: 1 addition & 13 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,6 @@ parameters:
count: 1
path: src/Reader/BodyReader.php

-
message: '#^Strict comparison using \=\=\= between '''' and '''' will always evaluate to true\.$#'
identifier: identical.alwaysTrue
count: 1
path: src/Reader/BodyReader.php

-
message: '#^Strict comparison using \=\=\= between array\{\} and array\{\} will always evaluate to true\.$#'
identifier: identical.alwaysTrue
Expand All @@ -99,7 +93,7 @@ parameters:
-
message: '#^Unreachable statement \- code above always terminates\.$#'
identifier: deadCode.unreachable
count: 2
count: 1
path: src/Reader/BodyReader.php

-
Expand All @@ -114,12 +108,6 @@ parameters:
count: 1
path: src/Reader/DocxReader.php

-
message: '#^Parameter \#1 \$byNumId of class Dskripchenko\\PhpDocx\\Reader\\NumberingDefinitions constructor expects array\<int, array\<int, array\{format\: Dskripchenko\\PhpDocx\\Element\\ListFormat, startAt\: int\}\>\>, array\<int, array\<int, array\{format\?\: Dskripchenko\\PhpDocx\\Element\\ListFormat, startAt\: int\}\>\> given\.$#'
identifier: argument.type
count: 1
path: src/Reader/NumberingReader.php

-
message: '#^Parameter \#1 \$key of function array_key_exists expects int\|string, string\|null given\.$#'
identifier: argument.type
Expand Down
6 changes: 5 additions & 1 deletion src/Element/Footnote.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
*/
final readonly class Footnote implements InlineElement
{
public function __construct(public string $content) {}
public function __construct(
public string $content,
/** The size the note is set in at the foot of the page; null for the consumer's own. */
public ?float $sizePt = null,
) {}
}
7 changes: 7 additions & 0 deletions src/Element/ListItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,12 @@ public function __construct(
* diverged from the original in how it broke into pages.
*/
public ParagraphStyle $style = new ParagraphStyle,
/**
* An item that only carries a nested list and shows no label of its
* own: it stands for a parent item outside this list, so that the
* nested one numbers on from it (`3.1.` after a paragraph that broke
* the list off at `3.`).
*/
public bool $markerHidden = false,
) {}
}
15 changes: 15 additions & 0 deletions src/Element/ListNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,28 @@
* $ordered.
* @param int $startAt The number to start from (default 1). Ignored for
* bullet lists.
* @param string|null $levelText Word's label pattern for this level
* (`%1.%2.`, `4.%1.`): `%N` is the
* number of level N. Null — the plain
* `%1.` of the format.
* @param string|null $markerClass The class that names this list
* level in HTML: the label rule hangs
* on it.
*/
public function __construct(
public array $items,
public bool $ordered = false,
public int $levelStart = 0,
public ?ListFormat $format = null,
public int $startAt = 1,
public ?string $levelText = null,
public ?string $markerClass = null,
/**
* Where the level's text starts, in twips from the margin — Word
* places a level by its numbering, not by how deep it is nested.
* Null for the plain nesting indent.
*/
public ?int $indentLeftTwips = null,
) {}

public function effectiveFormat(): ListFormat
Expand Down
Loading
Loading