Skip to content

Commit 6464ab6

Browse files
committed
Clean up contributor docs
1 parent d218981 commit 6464ab6

2 files changed

Lines changed: 163 additions & 27 deletions

File tree

AGENTS.md

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
1-
# Repository Guidelines
1+
# Agent guidelines
2+
3+
> For the human-readable contributor guide, see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). This file is a concise quick-reference for the AI assistant.
24
35
## Project structure & module organization
46

57
Source code lives in `draftjs_exporter/`. Tests are in `tests/`. Contributor and user docs are in `docs/`. Type-checking stubs live in `stubs/`. The root also contains `example.py` and `benchmark.py` for local runs and performance checks.
68

9+
See [docs/CONTRIBUTING.md#project-architecture](docs/CONTRIBUTING.md#project-architecture) for the full architecture walkthrough.
10+
711
## Development commands
812

9-
- `just help`: See what commands are available.
10-
- `just init`: Install dependencies and initialise for development.
11-
- `just lint`: Lint the project.
12-
- `just format`: Format project files.
13-
- `just test`: Test the project.
14-
- `just test-watch`: Restarts the tests whenever a file changes.
15-
- `just test-coverage`: Run the tests while generating test coverage data.
16-
- `just test-compatibility`: Compatibility-focused test suite.
17-
- `just dev`: Restarts the example whenever a file changes.
18-
- `just benchmark`: Runs a one-off performance (speed, memory) benchmark.
19-
- `just clean-pyc`: Remove Python file artifacts.
20-
- `just build`: Builds package for publication.
21-
- `just publish`: Publishes a new version to PyPI.
13+
See [docs/CONTRIBUTING.md#commands](docs/CONTRIBUTING.md#commands) for the full list. Key recipes:
14+
15+
- `just init` – install dependencies
16+
- `just lint` – lint + type-check
17+
- `just format` – auto-format
18+
- `just test` – run tests (strict mode)
19+
- `just test-coverage` – run with coverage
2220

2321
## Project tools
2422

@@ -32,19 +30,27 @@ Source code lives in `draftjs_exporter/`. Tests are in `tests/`. Contributor and
3230

3331
## Coding style & naming conventions
3432

35-
- Python uses 4-space indentation and type annotations checked with mypy / ty.
36-
- Formatting is enforced by `ruff format` for Python and `prettier` for all other files.
37-
- Test modules follow `test_*.py`, with test functions named `test_*`.
33+
See [docs/CONTRIBUTING.md#coding-style--conventions](docs/CONTRIBUTING.md#coding-style--conventions) for the full guide. Quick points:
34+
35+
- Python uses 4-space indentation, [PEP 8](https://peps.python.org/pep-0008/) style, enforced with `ruff`.
36+
- Type annotations required on production code (checked by mypy, ty). Test code is exempt.
37+
- Formatting: `ruff format` for Python, `prettier` for all other files.
38+
- Test modules follow `test_*.py`, with test functions named `test_*`, test classes `Test*`.
39+
- Core classes must use `__slots__`.
3840

3941
## Testing guidelines
4042

43+
See [docs/CONTRIBUTING.md#testing](docs/CONTRIBUTING.md#testing) for the full guide. Quick points:
44+
4145
- Target of 100% test coverage for all improvements.
42-
- Write tests both at the unit level but also integration (`test_exports.py`, `test_output.py`)
43-
- Write implementation and language-agnostic test cases in `test_exports.json`
46+
- Write tests at the unit level, integration level (`test_output.py`), and snapshot level (`test_exports.json`).
47+
- Add or update test cases in `test_exports.json` when output behavior changes across engines.
4448

4549
## Commit & pull request guidelines
4650

47-
- Be concise and to the point. Explain rationales that aren’t obvious.
51+
See [docs/CONTRIBUTING.md#pull-request-workflow](docs/CONTRIBUTING.md#pull-request-workflow). Quick points:
52+
53+
- Be concise and to the point. Explain rationales that aren't obvious.
4854
- No Title Case usage ever. Always use Sentence case.
49-
- Recent commit messages use short, capitalized, imperative summaries (e.g., Enforce additional mypy check).
55+
- Recent commit messages use short, capitalized, imperative summaries (e.g., "Enforce additional mypy check").
5056
- PRs should include a clear description, relevant test evidence (command + result), links to any related issues.

docs/CONTRIBUTING.md

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,72 @@
22

33
Thank you for considering to help this project.
44

5-
We welcome all support, whether on bug reports, code, design, reviews, tests, documentation, and more. Check out the [project roadmap](../ROADMAP.md) for high-level ideas that align with the projects goals.
5+
We welcome all support, whether on bug reports, code, design, reviews, tests, documentation, and more. Check out the [project roadmap](../ROADMAP.md) for high-level ideas that align with the project's goals.
66

77
Please note that this project is released with a [Contributor Code of Conduct](docs/CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.
88

9+
## Project architecture
10+
11+
Understanding the high-level architecture will help you navigate the codebase and find where to make changes.
12+
13+
### Module organization
14+
15+
```ba
16+
draftjs_exporter/
17+
html.py # Entry point. HTML class orchestrates the full rendering pipeline.
18+
dom.py # Virtual DOM abstraction. Static facade over interchangeable engines.
19+
command.py # Command model – operations derived from Draft.js ranges.
20+
entity_state.py # Entity (link, image, embed) rendering state machine.
21+
style_state.py # Inline style (bold, italic) nesting state machine.
22+
wrapper_state.py # Block wrapper nesting (e.g. <ul>/<ol> around <li>).
23+
options.py # Configuration normalization – converts user config to internal format.
24+
composite_decorators.py # Regex-based text decorators (line breaks, mentions, linkify).
25+
constants.py # BLOCK_TYPES, ENTITY_TYPES, INLINE_STYLES enums.
26+
defaults.py # Default BLOCK_MAP and STYLE_MAP for HTML.
27+
types.py # Type aliases and TypedDicts for the public API.
28+
error.py # Exception base classes.
29+
engines/
30+
base.py # DOMEngine abstract base class.
31+
string.py # String-concatenation engine (fast, no dependencies, default).
32+
string_compat.py # Backward-compatible variant of the string engine.
33+
html5lib.py # BeautifulSoup / html5lib engine.
34+
lxml.py # lxml engine.
35+
markdown.py # Non-escaping string engine for Markdown output.
36+
markdown/ # Markdown-specific components, config builder, and helpers.
37+
utils/
38+
module_loading.py # import_string() for dotted-path class resolution.
39+
```
40+
41+
Tests mirror this structure under `tests/`, with sub-packages for `engines/`, `markdown/`, and `utils/`.
42+
43+
### Rendering pipeline
44+
45+
The core flow lives in `HTML.render()` and proceeds as follows:
46+
47+
1. **Engine setup**`DOM.engine()` resolves the engine string to a class (via `import_string`), caches it, and sets it in a thread-safe `ContextVar`.
48+
2. **Option normalisation**`Options.map_blocks()`, `Options.map_styles()`, `Options.map_entities()` convert the user-friendly config maps into flat `OptionsMap` dicts of type → normalized `Options` objects.
49+
3. **Block iteration**`HTML.render()` creates a `WrapperState` instance and an empty document fragment, then iterates over each block.
50+
4. **Per-block rendering** – For each block, `render_block()` extracts text, inline styles, entity ranges, and composite decorators. It builds a sorted list of `Command` objects from the Draft.js ranges, groups consecutive commands by character offset, and processes each group through:
51+
- **EntityState** – manages an entity stack; on `start_entity`/`stop_entity` pairs it wraps children in entity components.
52+
- **StyleState** – tracks active inline styles; `render_styles()` wraps text in nested inline elements from innermost to outermost.
53+
- **Composite decorators** – regex-based text transformations (e.g. `\n``<br>`).
54+
5. **Wrapper resolution**`wrapper_state.element_for()` resolves each block to a DOM element, managing nesting of wrapper elements (e.g. `<ul>`/`<ol>` for list items) based on block depth.
55+
6. **Final rendering** – All block elements are appended to the document fragment, then `DOM.render()` serialises the virtual DOM tree to the output string (HTML or Markdown).
56+
57+
### Engine system
58+
59+
The exporter uses a **Strategy pattern** for output generation. All engines implement the `DOMEngine` interface (five static methods: `create_tag`, `parse_html`, `append_child`, `render`, `render_debug`). The `DOM` class delegates to the active engine, selected at runtime via dotted-path strings stored as class constants (`DOM.STRING`, `DOM.HTML5LIB`, `DOM.LXML`, `DOM.MARKDOWN`, `DOM.STRING_COMPAT`). Engine selection is thread-safe through a `ContextVar`.
60+
61+
### Key design patterns
62+
63+
- **Strategy** – interchangeable engines selected at runtime.
64+
- **Facade**`DOM` class hides engine-specific details.
65+
- **State machine**`EntityState` tracks entity open/close via a stack.
66+
- **Command pattern** – operations on text are modelled as `Command` objects, sorted, grouped, and applied in order.
67+
- **Pipeline** – text passes through decorators → inline styles → entity wrapping → wrapper nesting, each stage wrapping the previous.
68+
- **Null object**`WrapperStack.head()` returns a default `Wrapper(-1)` when the stack is empty.
69+
- **Slots for performance** – core classes use `__slots__` to reduce memory overhead.
70+
971
## Development
1072

1173
### Installation
@@ -39,13 +101,85 @@ just init
39101
- `just build`: Builds package for publication.
40102
- `just publish`: Publishes a new version to PyPI.
41103

104+
### Dependencies
105+
106+
This project uses multiple package managers and an automated dependency bot.
107+
108+
**Python** – managed with [uv](https://github.com/astral-sh/uv). Runtime dependencies are optional extras only (`lxml`, `html5lib`).
109+
110+
**JavaScript (tooling only)** – managed with `npm`. Only `prettier` is used, for formatting non-Python files. Locked in `package-lock.json`. Install with `npm install`.
111+
112+
**Dependency updates** – handled by [Renovate](https://docs.renovatebot.com/):
113+
114+
- Runs on a bi-weekly schedule (3rd and 22nd of each month).
115+
- PRs are labelled `dependencies`.
116+
- A 14-day minimum release age ensures stability before updates are proposed.
117+
- `uv.lock` is refreshed weekly (Mondays) to catch transitive dependency updates.
118+
- GitHub Actions, npm, and most Python dependencies are auto-merged.
119+
- `lxml`, `beautifulsoup4`, and `html5lib` are excluded from automated updates – their version bounds are intentionally conservative and must be updated manually after verifying there are no output changes.
120+
121+
To manually update a dependency, edit the version in `pyproject.toml` or `package.json`, then run `uv sync --dev` or `npm install` to update the lockfile. Run `just test-compatibility` to verify the project works with the lower-bound dependency versions declared for optional extras.
122+
42123
### Debugging
43124

44125
- Always run the tests. To auto-run with watch, use `npm install -g nodemon`, then `just test-watch`.
45126
- Use a debugger. `uv pip install ipdb`, then `import ipdb; ipdb.set_trace()`.
46127
- You can use `example.py` as a basic CLI to try out the exporter with arbitrary ContentState JSON: `echo '{"json": "contents"}' | ./example.py -`.
128+
- Inspect the DOM tree at any stage using `DOM.render_debug()` to see the virtual DOM structure before serialisation.
129+
- Run individual test files with `uv run pytest tests/test_dom.py`, or filter with `-k`: `uv run pytest tests/test_dom.py -k "test_create_element"`.
130+
- Use `just dev` to restart the example automatically whenever source files change.
47131

48-
### Releases
132+
## Coding style & conventions
133+
134+
We follow [PEP 8](https://peps.python.org/pep-0008/) for Python code style, enforced automatically by `ruff`:
135+
136+
- **Python**: formatted with `ruff format`, linted with `ruff check`. Configuration in `pyproject.toml`.
137+
- **Other files**: formatted with `prettier` (see `prettier.config.js`).
138+
- **Indentation**: 4 spaces, no tabs.
139+
- **Type annotations**: required on all production code, checked by `mypy` with strict settings and by `ty` (experimental).
140+
- **Naming**: `snake_case` for functions, methods, and variables; `PascalCase` for classes; `UPPER_CASE` for constants. Test modules follow `test_*.py`, test functions `test_*`, test classes `Test*`.
141+
- **Performance**: core classes should use `__slots__` to reduce memory overhead.
142+
- **Imports**: organised automatically by `ruff` (isort rules in `pyproject.toml`).
143+
- **Error handling**: use specific exception types; avoid bare `except:` clauses (BLE rules).
144+
145+
## Testing
146+
147+
We aim for 100% test coverage on all changes. Tests are run with `pytest` and configured in `pyproject.toml`.
148+
149+
### Test organization
150+
151+
Tests mirror the source layout:
152+
153+
- `tests/test_<module>.py` – unit tests for each module under `draftjs_exporter/`.
154+
- `tests/engines/test_engines_*.py` – engine-specific unit tests.
155+
- `tests/markdown/test_*.py` – markdown-specific unit tests (auto-switch to the MARKDOWN engine via `conftest.py`).
156+
- `tests/utils/test_*.py` – utility tests.
157+
158+
### Types of tests
159+
160+
The project has a layered test suite to increase the opportunities to catch bugs.
161+
162+
- **Unit tests** – individual classes and functions in isolation (most test files). Use `unittest.TestCase` style with `setUp`/`tearDown`.
163+
- **Integration tests**`test_output.py` exercises the full pipeline end-to-end with complex content states (49 KB, 1284 lines).
164+
- **Data-driven / snapshot tests**`test_exports.py` reads `test_exports.json` at module load time via a custom metaclass (`ExportsTestMeta`) and dynamically generates test methods for every engine × test case combination. Add a new test case to `test_exports.json` when you want to verify output across all engines.
165+
- **Engine difference tests**`test_engines_differences.py` compares outputs between engines to catch regressions.
166+
167+
### Writing tests
168+
169+
- Add unit tests alongside the module you are changing, following the existing patterns.
170+
- Add cross-engine test cases to `test_exports.json` when adding or modifying output behavior. Each test case needs a `label`, a `content_state`, and expected `output` for all five engines.
171+
- All output changes should be covered with unit tests, and integration tests, and snapshot tests.
172+
173+
## Pull request workflow
174+
175+
1. Create a branch from `main` with a descriptive name.
176+
2. Make your changes, following the coding style and testing guidelines above.
177+
3. Run `just lint` and `just test` locally to verify everything passes.
178+
4. Open a pull request with a clear description of what the change does and why. Include relevant test evidence (commands and their output) and links to related issues.
179+
5. CI will run linting (ruff, mypy, ty, prettier), benchmarks, test coverage, and the compatibility test suite. All checks must pass before merging.
180+
6. Squash merge when approved. Keep the commit message concise, in the imperative mood, and using Sentence case (no Title Case).
181+
182+
## Releases
49183

50184
- Make a new branch for the release of the new version.
51185
- Update the [CHANGELOG](https://github.com/wagtail/draftjs_exporter/CHANGELOG.md).
@@ -77,7 +211,3 @@ env PYTHON_CONFIGURE_OPTS='--enable-optimizations --with-lto' PYTHON_CFLAGS='-ma
77211
### Static typing
78212

79213
All exporter code should pass static type checking by [mypy](https://mypy.readthedocs.io/en/latest/index.html), with as strict of a configuration as possible, and tentatively also pass type checks with the [ty](https://docs.astral.sh/ty/) checker.
80-
81-
## Documentation
82-
83-
> See the [docs](https://github.com/wagtail/draftjs_exporter/tree/main/docs) folder.

0 commit comments

Comments
 (0)