Skip to content

Commit 403d871

Browse files
committed
added AGENTS.md & DOCS
1 parent f5245ae commit 403d871

14 files changed

Lines changed: 1872 additions & 414 deletions

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
.gitattributes export-ignore
22
.github/ export-ignore
33
.gitignore export-ignore
4+
AGENTS.md export-ignore
45
ncs.* export-ignore
56
phpstan*.neon export-ignore
7+
docs/ export-ignore
68
tests/ export-ignore
79

810
*.php* diff=php

AGENTS.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# To My Agents!
2+
3+
It is my fervent wish that this file guide every AI coding agent working with code in this repository.
4+
5+
## Documentation
6+
7+
Any distilled, agent-facing documentation for this package - how it works
8+
internally and the rationale behind key design decisions - lives in `docs/`.
9+
Consult it before non-trivial changes; it is the source of truth from which the
10+
public manual is distilled.
11+
12+
The core is non-trivial - a two-parser pipeline, a protection-mark hierarchy,
13+
chain-of-responsibility handlers, and DTD validation. Read the relevant
14+
`docs/` seam before editing. Note the internals describe the current
15+
**Texy 3.x** line; 4.0 will be AST-based and change most of it.
16+
17+
## Project Overview
18+
19+
Texy is a mature text-to-HTML converter: it turns plain text in Texy syntax into
20+
valid (X)HTML with typography, images, links, tables, and lists, and integrates
21+
with Latte.
22+
23+
- **PHP Version**: 8.1 - 8.5
24+
- **Package**: `texy/texy`
25+
26+
## Essential Commands
27+
28+
```bash
29+
# Run all tests
30+
vendor/bin/tester tests -s # or: composer tester
31+
vendor/bin/tester tests/Texy/blocks.phpt -s
32+
33+
# Static analysis (PHPStan level 5, informative)
34+
composer phpstan
35+
```
36+
37+
## Conventions
38+
39+
- Every file starts with `declare(strict_types=1);`; Nette Coding Standard.
40+
- Tests are Nette Tester `.phpt` comparing output against `tests/Texy/expected/*.html`
41+
from `tests/Texy/sources/*.texy` (via `Assert::matchFile`). Test files are named
42+
**`{subject}[-{aspect}].phpt`** (singular subject like `image`; aspect like
43+
`-reference`/`-handler`/`-syntax`), and expected files use descriptive suffixes,
44+
not numbers (`figure-nocaption.html`, not `figure2.html`).
45+
46+
## Working in this repo
47+
48+
- **Two parsers, in order:** `BlockParser` handles block structures (blocks never
49+
overlap), then `LineParser` handles inline syntaxes (nesting via progressive
50+
expansion). See `docs/parsing.md`.
51+
- **Protection marks are a hierarchy, not just a mask.** Content-type bytes
52+
`\x14`-`\x1F` are ordered so `[\x17-\x1F]+` matches a whole MARKUP placeholder;
53+
paragraph detection, autolinks, longwords, and typography all read them. New
54+
patterns must exclude already-processed content (`[^\x14-\x1F]`), and raw HTML you
55+
emit must be wrapped via `$texy->protect($html, Texy\Texy::CONTENT_BLOCK)`.
56+
- **Syntax collisions are resolved by registration order** (earlier pattern wins);
57+
some registration is lazy in `beforeParse`.
58+
- **The handler chain runs last-registered-first**, with the module's default
59+
implementation last; a handler calls `$invocation->proceed()` to delegate.
60+
- **Modules are wired one-directionally through value objects** (`Link`/`Image`);
61+
`HtmlOutputModule` fixes nesting/auto-closing, `Modifier::decorate` filters against
62+
the `allowed*` whitelists, and `HtmlElement` is DTD-validated.
63+
- **Security: always run `Configurator::safeMode($texy)` for untrusted input** - it
64+
restricts HTML to a safe subset, disables classes/IDs/styles and images, filters
65+
URL schemes, and adds `rel="nofollow"`.
66+
- User- and extender-facing how-to (Texy syntax, configuration, custom handlers,
67+
custom syntax registration, the modifier catalog) is manual material and lives in
68+
the public web docs, not here.

docs/architecture.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Architecture and Principles
2+
3+
Texy converts text written in its own markup language to HTML. Unlike simple converters that process text linearly with a series of replacements, Texy uses a system based on parsing, a modular architecture, and incremental building of a DOM tree.
4+
5+
Processing runs in four main phases:
6+
7+
1. **Preprocessing** – normalization of line endings and spaces, tab expansion, removal of soft hyphens, invocation of `beforeParse` notification handlers.
8+
2. **Parsing** – recognition of syntaxes using regular expressions and incremental building of a DOM tree of `HtmlElement` objects.
9+
3. **Post-processing** – typographic corrections, long-word hyphenation, HTML well-forming.
10+
4. **Final assembly** – conversion of the DOM tree into the resulting HTML string.
11+
12+
The key difference from naive approaches is the separation of syntax *recognition* from syntax *processing*. The parser first identifies where each syntactic construct occurs in the text and only then hands the found parts over to individual modules. This allows syntaxes to nest and to be unwrapped step by step.
13+
14+
All classes live in the `Texy` namespace (so `HtmlElement` means `Texy\HtmlElement`); modules live in `Texy\Modules`.
15+
16+
## Key components
17+
18+
**The `Texy` class** (`src/Texy/Texy.php`) is the central orchestrator. It holds references to all modules, manages registered syntaxes and handlers, maintains processing state, and coordinates the conversion phases. It is the single place where components are wired together.
19+
20+
**[Modules](modules.md)** are functional units responsible for specific areas of the markup language. Each module registers, in its constructor, the syntaxes it recognizes and the element handlers that process them. For example `PhraseModule` handles inline formatting such as bold or italic text, while `TableModule` handles tables. Modules are designed as self-contained, reusable units with their own configuration exposed as public properties.
21+
22+
**[Parsers](parsing.md)** come in two variants by content type. `BlockParser` processes block structures such as paragraphs, headings, lists, or tables: it walks the text line by line, looks for the beginnings of block constructs, and passes them to *syntax handlers*. `LineParser` handles inline syntaxes within lines – links, images, text formatting. Unlike `BlockParser`, it supports nesting of syntaxes and their gradual unwrapping.
23+
24+
## Terminology
25+
26+
To understand Texy you need to distinguish several key terms that recur throughout this documentation.
27+
28+
**Syntax** is a named syntactic construct of the markup language. Every syntax has a unique name, e.g. `phrase/strong` for bold text or `image` for images. The name is used to enable or disable the syntax in the `Texy::$allowed` array, and it is passed to syntax handlers so a shared callback can tell which syntax matched.
29+
30+
**Pattern** is the regular expression that defines what the syntax looks like in text. The pattern is an implementation detail of the syntax – the author must write a regex that recognizes it, but from the user's perspective the syntax name and meaning matter more. One module typically registers several syntaxes with different patterns.
31+
32+
**Syntax handler** is the function the parser calls when it finds an occurrence of the syntax in text. It receives the matched text and returns an `HtmlElement` or a string that is inserted back in place of the match. The syntax handler decides what happens with the found construct – typically it invokes an element handler for the actual processing.
33+
34+
**Element** is a kind of item for which an HTML representation is generated. For example `image` is the element for images, `linkURL` for autodetected links, `phrase` for inline formatting. Each element has a default element handler implemented in the owning module.
35+
36+
**Element handler** is a function registered for a given element type and invoked through the `HandlerInvocation` system. Its distinguishing feature is the `proceed()` method, which delegates to the next handler in the chain or to the module's default handler. Element handlers modify or replace the default behavior.
37+
38+
**Notification handler** is a function called to signal an event. Unlike element handlers it returns nothing and cannot influence the result of processing. It is used for preparing data, logging, or modifying an already-built DOM tree.
39+
40+
The distinction is crucial: a syntax handler is tightly coupled to the parser and to a specific pattern – it answers *"what to do when the parser finds this pattern"*. Element handlers sit at a higher level of abstraction – they answer *"how to process this kind of item"*, regardless of which concrete syntax produced it.
41+
42+
## Overall processing flow
43+
44+
When `Texy::process()` receives input text, the following happens (`src/Texy/Texy.php`, method `process()`):
45+
46+
1. **Preprocessing.** Soft hyphens (U+00AD) are removed (if `$removeSoftHyphens` is on), line endings and spaces are normalized (`Helpers::normalize()`), and tabs are expanded to spaces according to `$tabWidth`. Then the `beforeParse` notification handlers are invoked with the text passed by reference – they can preprocess data, e.g. `LinkModule` and `ImageModule` extract reference definitions here and `TypographyModule` prepares locale-specific patterns.
47+
48+
2. **Pattern selection.** The registered line and block patterns are filtered by the `$allowed` array. This happens once per `process()` call, so changing `$allowed` during processing has no effect.
49+
50+
3. **Parsing.** A root `HtmlElement` representing the document is created. For a full document, `parseBlock()` creates a `BlockParser` that walks the text and identifies block constructs; text between blocks is handled by `ParagraphModule`, which internally uses `LineParser` for the inline content. For `processLine()`, only `parseLine()`/`LineParser` is used. Parsing incrementally builds the DOM tree.
51+
52+
4. **afterParse.** After parsing completes, `afterParse` notification handlers are invoked with the root element. They can perform final tree adjustments, e.g. `HeadingModule` assigns final heading levels, generates IDs and builds the TOC here.
53+
54+
5. **Serialization and post-processing.** `HtmlElement::toHtml()` converts the tree to a string. During this conversion each element is recursively rendered with HTML tags immediately masked by protection marks (see [parsing.md](parsing.md#protection-marks)). The resulting internal string is passed through `Texy::stringToHtml()`, which:
55+
- applies **post-line handlers** registered via `registerPostLine()` – typographic corrections (`typography`) and long-word hyphenation (`longwords`) – to the textual segments between block marks,
56+
- escapes `<`, `>`, `&` in remaining text,
57+
- replaces all protection marks with their real values (`unprotect()`),
58+
- invokes the `postProcess` notification event, which `HtmlOutputModule` uses to well-form and reformat the HTML (closing tags, fixing wrong nesting, indentation, line wrapping),
59+
- unfreezes spaces in attributes (see below).
60+
61+
The result is the final HTML string.
62+
63+
## Syntax system
64+
65+
A syntax is an abstract concept combining a unique name, a regular expression for recognition, and a way of processing. The name serves as the identifier throughout the system – in `Texy::$allowed`, in handler parameters, in documentation.
66+
67+
Naming follows two conventions. Simple syntaxes have a one-word name matching their purpose: `image`, `table`, `script`. More complex areas use hierarchical names with a slash: `phrase/strong`, `phrase/em`, `link/reference`. The slash groups related syntaxes logically.
68+
69+
There are three kinds of syntaxes, each with its own registration method on `Texy`:
70+
71+
- **Line syntaxes** (`registerLinePattern()`) recognize inline items within lines of text – formatting, links, images, inline code. They may nest inside each other and [`LineParser`](parsing.md#lineparser) unwraps them gradually. Their patterns are searched for anywhere in the text, so they must not be anchored.
72+
- **Block syntaxes** (`registerBlockPattern()`) recognize multi-line block constructs: headings, lists, tables, quotes, special blocks. Unlike line syntaxes, block syntaxes never overlap – every line of text belongs to at most one block construct, and [`BlockParser`](parsing.md#blockparser) processes them without interleaving. Their patterns are anchored to the start of a line (the `m` modifier is added automatically).
73+
- **Post-line syntaxes** (`registerPostLine()`) do not parse markup at all; they transform the final textual content between block-level protection marks just before HTML entities are encoded. Two modules use it: `TypographyModule` (name `typography`) and `LongWordsModule` (name `longwords`).
74+
75+
In all three cases the registered *syntax handler* returns an `HtmlElement`, a string, or `null` to refuse processing (post-line handlers return the transformed string). Registration parameters and handler signatures are documented in detail in the custom-syntax guide (user manual).
76+
77+
### Enabling and disabling syntaxes
78+
79+
The `Texy::$allowed` array gives fine-grained control over which syntaxes are active:
80+
81+
```php
82+
$texy->allowed['phrase/strong'] = false;
83+
```
84+
85+
`registerLinePattern()`, `registerBlockPattern()` and `registerPostLine()` default the entry to `true` if it is not set yet; a module can explicitly default a syntax to `false` (e.g. `emoticon`, `phrase/ins`, `phrase/del`, `phrase/sup`, `phrase/sub`). The check happens once at the start of parsing, so changing `$allowed` mid-processing has no effect.
86+
87+
The complete list of syntaxes with their default states is in the syntax reference (user manual); safe-mode presets are described in the configuration reference (user manual).
88+
89+
## Handler system
90+
91+
### Element handlers
92+
93+
Element handlers implement the chain-of-responsibility pattern, allowing the resulting behavior to be composed from multiple layers.
94+
95+
Registration uses `Texy::addHandler($elementName, $callback)`. One element name may have multiple handlers; they execute in order **from the last registered to the first**. Since modules register their default handlers in their constructors, a user handler registered later gets control first and decides whether the default handler runs at all.
96+
97+
Element names identify the kind of processing: `phrase`, `image`, `block`, `heading`... Sometimes compound names distinguish flavors, e.g. `linkReference`, `linkEmail`, `linkURL`, `newReference`. Element names are more general than syntax names – the `phrase` element covers all inline formatting syntaxes.
98+
99+
Invocation goes through `Texy::invokeAroundHandlers($event, $parser, $args)`, which wraps the registered handlers in a `HandlerInvocation` object (`src/Texy/HandlerInvocation.php`). The handler receives the `HandlerInvocation` as its first parameter followed by element-specific arguments, and controls the chain through `$invocation->proceed()`: calling it delegates onward (optionally with replaced parameters), not calling it breaks the chain.
100+
101+
See the custom-handlers guide (user manual) for the exact `proceed()` semantics, the full reference of elements with signatures, and practical examples.
102+
103+
### Notification handlers
104+
105+
Notification handlers use the same registration method, `Texy::addHandler($eventName, $callback)`, but are invoked with `Texy::invokeHandlers()`, which simply calls all registered handlers in registration order and ignores their return values. Handlers receive the invocation arguments but cannot change them for the following handlers (except for parameters explicitly passed by reference, such as the text in `beforeParse`).
106+
107+
The events are: `beforeParse`, `afterParse`, `beforeBlockParse`, `afterTable`, `afterList`, `afterDefinitionList`, `afterBlockquote`, and `postProcess`. Signatures are listed in the custom-handlers guide (user manual).
108+
109+
Unlike element handlers, notification handlers cannot prevent further processing – all registered handlers always run. That is intentional: notifications are about side effects, not flow control.
110+
111+
## Space freezing
112+
113+
To prevent line wrapping and other post-processing from corrupting HTML attribute values, spaces inside attributes are "frozen" during serialization using `Helpers::freezeSpaces()` (space → `\x01`, tab → `\x02`, `\r``\x03`, `\n``\x04`) and restored at the very end by `Helpers::unfreezeSpaces()` in `stringToHtml()`.
114+

0 commit comments

Comments
 (0)