latexwalker 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. latexwalker-0.1.0/.github/workflows/ci.yml +34 -0
  2. latexwalker-0.1.0/.github/workflows/docs.yml +52 -0
  3. latexwalker-0.1.0/.github/workflows/publish.yml +28 -0
  4. latexwalker-0.1.0/.gitignore +21 -0
  5. latexwalker-0.1.0/.opencode/opencode.jsonc +9 -0
  6. latexwalker-0.1.0/.opencode/skills/context7-usage/SKILL.md +129 -0
  7. latexwalker-0.1.0/.pre-commit-config.yaml +38 -0
  8. latexwalker-0.1.0/.python-version +1 -0
  9. latexwalker-0.1.0/AGENTS.md +165 -0
  10. latexwalker-0.1.0/ARCHITECTURE.md +108 -0
  11. latexwalker-0.1.0/CONTRIBUTING.md +124 -0
  12. latexwalker-0.1.0/LICENSE +7 -0
  13. latexwalker-0.1.0/PKG-INFO +66 -0
  14. latexwalker-0.1.0/README.md +54 -0
  15. latexwalker-0.1.0/docs/api.rst +103 -0
  16. latexwalker-0.1.0/docs/conf.py +45 -0
  17. latexwalker-0.1.0/docs/index.rst +56 -0
  18. latexwalker-0.1.0/justfile +49 -0
  19. latexwalker-0.1.0/pyproject.toml +67 -0
  20. latexwalker-0.1.0/src/latexwalker/__init__.py +72 -0
  21. latexwalker-0.1.0/src/latexwalker/_version.py +24 -0
  22. latexwalker-0.1.0/src/latexwalker/errors.py +24 -0
  23. latexwalker-0.1.0/src/latexwalker/lexer.py +129 -0
  24. latexwalker-0.1.0/src/latexwalker/nodes.py +123 -0
  25. latexwalker-0.1.0/src/latexwalker/normalize.py +46 -0
  26. latexwalker-0.1.0/src/latexwalker/parser.py +288 -0
  27. latexwalker-0.1.0/src/latexwalker/resources/rules.yaml +49 -0
  28. latexwalker-0.1.0/src/latexwalker/rewrite.py +196 -0
  29. latexwalker-0.1.0/src/latexwalker/rules.py +54 -0
  30. latexwalker-0.1.0/src/latexwalker/source.py +138 -0
  31. latexwalker-0.1.0/src/latexwalker/tokens.py +41 -0
  32. latexwalker-0.1.0/src/latexwalker/traverse.py +38 -0
  33. latexwalker-0.1.0/src/latexwalker/zones.py +78 -0
  34. latexwalker-0.1.0/tests/__init__.py +0 -0
  35. latexwalker-0.1.0/tests/test_api.py +86 -0
  36. latexwalker-0.1.0/tests/test_lexer.py +163 -0
  37. latexwalker-0.1.0/tests/test_nodes.py +110 -0
  38. latexwalker-0.1.0/tests/test_normalize.py +102 -0
  39. latexwalker-0.1.0/tests/test_parser.py +186 -0
  40. latexwalker-0.1.0/tests/test_rewrite.py +153 -0
  41. latexwalker-0.1.0/tests/test_rules.py +79 -0
  42. latexwalker-0.1.0/tests/test_source.py +161 -0
  43. latexwalker-0.1.0/tests/test_zones.py +135 -0
  44. latexwalker-0.1.0/uv.lock +1099 -0
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ quality:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: astral-sh/setup-uv@v6
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install deps
22
+ run: uv sync --dev --locked
23
+
24
+ - name: Ruff lint
25
+ run: uv run ruff check . --output-format=github
26
+
27
+ - name: Ruff format
28
+ run: uv run ruff format . --check
29
+
30
+ - name: basedpyright
31
+ run: uv run basedpyright
32
+
33
+ - name: Pytest
34
+ run: uv run pytest -v
@@ -0,0 +1,52 @@
1
+ name: Deploy documentation
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+ pages: write
12
+ id-token: write
13
+
14
+ concurrency:
15
+ group: "pages"
16
+ cancel-in-progress: false
17
+
18
+ jobs:
19
+ build:
20
+ name: Build docs
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - uses: astral-sh/setup-uv@v6
26
+ - uses: actions/setup-python@v5
27
+ with:
28
+ python-version: "3.12"
29
+
30
+ - name: Install deps
31
+ run: uv sync --no-dev --group docs --locked
32
+
33
+ - name: Build site
34
+ run: uv run sphinx-build -W --keep-going -b html docs docs/_build/html
35
+
36
+ - uses: actions/configure-pages@v5
37
+ - uses: actions/upload-pages-artifact@v3
38
+ with:
39
+ path: docs/_build/html
40
+
41
+ deploy:
42
+ name: Deploy docs
43
+ needs: build
44
+ runs-on: ubuntu-latest
45
+ environment:
46
+ name: github-pages
47
+ url: ${{ steps.deployment.outputs.page_url }}
48
+
49
+ steps:
50
+ - name: Deploy to GitHub Pages
51
+ id: deployment
52
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ id-token: write
14
+ contents: read
15
+ environment: pypi
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v6
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Build package
25
+ run: uv build
26
+
27
+ - name: Publish package
28
+ run: uv publish
@@ -0,0 +1,21 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ src/*/_version.py
9
+
10
+ # Virtual environments
11
+ .venv
12
+
13
+ # Cache
14
+ .pytest_cache/
15
+ .ruff_cache/
16
+
17
+ # Docs
18
+ docs/_build/
19
+
20
+ # Local
21
+ .vscode/
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "mcp": {
4
+ "context7": {
5
+ "type": "remote",
6
+ "url": "https://mcp.context7.com/mcp",
7
+ },
8
+ },
9
+ }
@@ -0,0 +1,129 @@
1
+ ---
2
+ name: context7-usage
3
+ description: Use Context7 to retrieve version-specific official documentation and examples for external libraries before implementing unfamiliar, version-sensitive, or API-dependent code.
4
+ ---
5
+
6
+ # Context7 usage
7
+
8
+ Use Context7 to verify third-party library APIs, configuration, migration paths,
9
+ and version-specific behavior. Prefer official documentation and primary project
10
+ sources over memory, search snippets, or guessed APIs.
11
+
12
+ ## When to use
13
+
14
+ Use Context7 when a task depends on:
15
+
16
+ - An unfamiliar library, framework, tool, or package API.
17
+ - Version-sensitive behavior, deprecations, or migration guidance.
18
+ - Exact constructor arguments, configuration keys, return types, or exception
19
+ behavior.
20
+ - Integration patterns for external dependencies such as msgspec, pytest, or
21
+ colorlog.
22
+ - A dependency addition or upgrade being considered.
23
+ - An implementation detail that cannot be established from this repository's code,
24
+ tests, type information, or lockfile.
25
+
26
+ Do not use Context7 when:
27
+
28
+ - The answer is already established by local code, tests, type stubs, or project
29
+ documentation.
30
+ - The task is internal refactoring with no external API dependency.
31
+ - The task is about repository conventions; follow `AGENTS.md`,
32
+ `ARCHITECTURE.md`, and `CONTRIBUTING.md` instead.
33
+ - You need current public information unrelated to a software-library API.
34
+
35
+ ## Workflow
36
+
37
+ 1. Read local usage first.
38
+ - Inspect imports, wrapper abstractions, configuration, and tests.
39
+ - Identify the installed package name and locked version before researching.
40
+ - Reuse an established project pattern when one exists.
41
+
42
+ 2. Resolve the library.
43
+ - Search Context7 using the package or project name.
44
+ - Select the official or canonical documentation source.
45
+ - Prefer a source matching the installed major and minor version.
46
+
47
+ 3. Query narrowly.
48
+ - Ask about one API surface at a time.
49
+ - Include the exact symbol, feature, or behavior being implemented.
50
+ - Request examples only when they clarify the required integration.
51
+
52
+ 4. Apply the result conservatively.
53
+ - Adapt documented patterns to local abstractions and architecture.
54
+ - Do not copy examples blindly; preserve local types, error handling, and test
55
+ conventions.
56
+ - Do not introduce a dependency or upgrade solely because an example uses it.
57
+
58
+ 5. Verify locally.
59
+ - Confirm the result with type checking and focused tests.
60
+ - Run the required project checks from `AGENTS.md` before completion.
61
+
62
+ ## Query patterns
63
+
64
+ Use concise, concrete queries.
65
+
66
+ ```text
67
+ msgspec Struct tagged union decode validation
68
+ pytest anyio async fixture scope
69
+ colorlog ColoredFormatter format reset log_color
70
+ ```
71
+
72
+ For version-sensitive behavior, include the known version:
73
+
74
+ ```text
75
+ msgspec 0.x Struct omit_defaults JSON schema
76
+ ```
77
+
78
+ Avoid broad queries such as:
79
+
80
+ ```text
81
+ how does msgspec work
82
+ best Python serialization library
83
+ show me all msgspec features
84
+ ```
85
+
86
+ ## Source selection
87
+
88
+ - Prefer official project documentation, official API references, and maintained
89
+ upstream repositories.
90
+ - Prefer the package's canonical source over third-party tutorials or blog posts.
91
+ - If Context7 returns multiple candidate libraries, verify the package identity
92
+ against the repository dependency manifest and imports before using one.
93
+ - Treat documentation for a different major version as potentially incompatible.
94
+ - If the installed version cannot be identified, state that uncertainty and avoid
95
+ relying on version-specific claims.
96
+
97
+ ## Dependency decisions
98
+
99
+ Context7 research does not authorize a dependency change.
100
+
101
+ Before proposing a new, removed, or upgraded dependency:
102
+
103
+ - Check whether the standard library or an existing dependency solves the problem.
104
+ - Identify the exact package, version policy, license, type support, maintenance
105
+ status, and transitive-dependency impact.
106
+ - Record alternatives considered in the plan.
107
+ - Obtain explicit approval before running `uv add`, removing a package, or changing
108
+ a version constraint.
109
+
110
+ ## Reporting
111
+
112
+ When Context7 materially informed an implementation plan or code change, report:
113
+
114
+ - The library and version investigated.
115
+ - The API or behavior verified.
116
+ - The local files or tests that use the result.
117
+ - Any version uncertainty, caveat, or deviation from the documented example.
118
+
119
+ Keep the report brief. Do not paste long documentation excerpts when a concise
120
+ statement and local verification result are sufficient.
121
+
122
+ ## Boundaries
123
+
124
+ - Context7 is evidence for external APIs, not a replacement for repository context.
125
+ - `AGENTS.md` controls code style, validation, dependencies, logging, and tests.
126
+ - `ARCHITECTURE.md` controls system boundaries, contracts, and invariants.
127
+ - `CONTRIBUTING.md` controls branches, commits, rebases, and review workflow.
128
+ - When Context7 conflicts with local project conventions, preserve local conventions
129
+ unless the task explicitly changes them.
@@ -0,0 +1,38 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v6.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ - id: end-of-file-fixer
7
+ - id: check-added-large-files
8
+
9
+ - repo: local
10
+ hooks:
11
+ - id: lint
12
+ name: lint
13
+ entry: just lint
14
+ language: system
15
+ types_or: [python, pyi]
16
+ pass_filenames: true
17
+
18
+ - id: format
19
+ name: format
20
+ entry: just format
21
+ language: system
22
+ types_or: [python, pyi]
23
+ pass_filenames: true
24
+
25
+ - id: typecheck
26
+ name: typecheck
27
+ entry: just typecheck
28
+ language: system
29
+ types_or: [python, pyi]
30
+ pass_filenames: false
31
+ stages: [pre-push]
32
+
33
+ - id: test
34
+ name: test
35
+ entry: just test
36
+ language: system
37
+ pass_filenames: false
38
+ stages: [pre-push]
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,165 @@
1
+ # AGENTS
2
+
3
+ ## Important files
4
+
5
+ Read:
6
+
7
+ - [README](/README.md) to understand the purpose of the project.
8
+ - [ARCHITECTURE](/ARCHITECTURE.md) to grasp the structure and conventions.
9
+ - [CONTRIBUTING](/CONTRIBUTING.md) for contribution guidelines.
10
+
11
+ ## Commands
12
+
13
+ - Use `just` as a task runner (see `just --summary` for the list of all tasks).
14
+ - Run `just lintcheck <paths>` to check linting.
15
+ - Run `just formatcheck <paths>` to check formatting.
16
+ - Run auto-fix before trying to fix issues manually:
17
+ `just format <paths>` (format fix), `just lint <paths>` (linting fix).
18
+ - Run `just typecheck <paths>` for static type-checking on selected files.
19
+ - Run `just test <pytest args>` for tests.
20
+ - Use `just check` to run all checks on the whole codebase.
21
+
22
+ You can use lightweight `just lint[check]` and `just format[check]` frequently.
23
+ Prefer focused `just typecheck` and `just test` over `just check`, for faster feedback.
24
+ To consider the task done, `just check` must pass without errors.
25
+
26
+ ## Dependencies
27
+
28
+ - Prefer the standard library and existing project dependencies before adding a new package; add a new dependency only when absolutely necessary.
29
+ - Use `uv add <package>` or `uv add --group <group> <package>` to modify
30
+ dependencies. Do not edit the lockfile manually.
31
+ - Commit the dependency manifest and lockfile together whenever dependencies change.
32
+ - Pin or constrain versions only when compatibility, reproducibility, or a known
33
+ upstream regression requires it; otherwise follow the project's existing
34
+ version policy.
35
+
36
+ ## Code style
37
+
38
+ - The code is written in Python 3.12.
39
+ - All code must be linted and formatted before commit.
40
+ - Depend on abstractions, not concrete backends; benefit from polymorphism.
41
+
42
+ ### Typing rules
43
+
44
+ - Fully-type the code; Annotate all functions and methods.
45
+ - Never use `typing.Any`; Never suppress diagnostics.
46
+ - Use structural typing via `typing.Protocol` instead of `abc.ABC`.
47
+ - If you need to add a base class, use `abc.ABC` alongside (not instead of) `typing.Protocol`.
48
+ - Use Python 3.12+ style generics.
49
+ - Run `just typecheck <paths>` for static type-checking (`basedpyright` strict).
50
+
51
+ When working with poorly typed third-party stubs never give up on types --
52
+ reason through them step-by-step.
53
+
54
+ ### Patterns
55
+
56
+ #### Control flow
57
+
58
+ - Prefer early returns over deeply nested conditionals.
59
+ - Prefer `match` when branching on a closed set of variants; use `if` only for
60
+ predicates.
61
+ - Prefer comprehensions for simple transformations; use named functions when the
62
+ transformation needs more than one clause.
63
+ - Prefer iterators and generators for streaming data; materialize collections only
64
+ when indexing, reuse, or batching requires it.
65
+
66
+ #### Function interfaces
67
+
68
+ - Use keyword-only arguments for parameters whose meaning is unclear at call sites.
69
+ - Represent optional values explicitly with `T | None`; validate or narrow them
70
+ immediately at the boundary where they are consumed.
71
+ - Prefer `Sequence[T]`, `Mapping[K, V]`, `Iterable[T]`, and `Collection[T]` in input
72
+ annotations unless mutation or a concrete operation requires `list` or `dict`.
73
+ - Accept the narrowest appropriate abstract collection type in inputs; return a
74
+ concrete immutable collection from public APIs when practical.
75
+ - Prefer returning new values over mutating inputs or shared state; permit in-place
76
+ mutation only when the function contract explicitly promises it.
77
+ - Never use mutable default arguments; use `None` and construct the value inside the
78
+ function.
79
+
80
+ #### Models and states
81
+
82
+ - Use `default_factory` for mutable dataclass fields.
83
+ - Use `msgspec.Struct` for schemas, serialization and where performance is crucial;
84
+ keep decoding and validation at system boundaries.
85
+ - Use `Enum` or discriminated union types for closed sets of states; do not represent
86
+ states with magic strings.
87
+ - Use `StrEnum` only when a string representation is part of a public boundary.
88
+ - Keep models that cross a boundary immutable unless mutation is essential to their
89
+ purpose.
90
+
91
+ #### Type narrowing
92
+
93
+ - Use `TypeIs` helper functions to narrow complex unions.
94
+ - Keep narrowing functions small, deterministic, and named as predicates.
95
+ - Use `typing.cast` only when the type equivalence is known and cannot be expressed
96
+ through normal narrowing.
97
+
98
+ #### Errors
99
+
100
+ - Catch only exceptions that can be handled meaningfully; keep `try` blocks minimal.
101
+ - Raise specific, contextual exceptions; chain external failures with
102
+ `raise ... from exc`.
103
+ - Keep error reporting at the boundary responsible for recovery or user-visible output.
104
+ - Make retries explicit and limited. Retry only transient, idempotent operations.
105
+ - Use exponential backoff with jitter for retryable external failures.
106
+ - Do not retry validation errors, malformed input, or authorization failures.
107
+ - For error logging, follow the [logging conventions](#logging) below.
108
+
109
+ #### Resources and I/O
110
+
111
+ - Use `pathlib.Path` for filesystem paths. Do not pass paths as raw strings internally.
112
+ - Use `contextlib` context managers for resources with explicit lifetimes.
113
+ - Close resources deterministically, including clients, files, streams, and database
114
+ sessions.
115
+ - Keep filesystem, network, database, and model calls at explicit boundaries.
116
+
117
+ #### Imports and modules
118
+
119
+ - Keep imports at module scope; use local imports only to avoid an import cycle or
120
+ defer an optional or expensive dependency.
121
+ - Keep modules focused on one cohesive concern. Split a module before it becomes a
122
+ catch-all for unrelated helpers.
123
+ - Avoid `utils`, `common`, and `helpers` modules. Name modules after the capability
124
+ they provide.
125
+ - Keep public exports explicit with `__all__` only for deliberately curated package APIs.
126
+
127
+ ## Comment style
128
+
129
+ Your code shall read like prose, even without comments:
130
+
131
+ - Use PEP 257 one-line docstrings.
132
+ - Module-level docstrings should describe the module's purpose, not its contents.
133
+ - If something (e.g. module, class, function) cannot be explained with a
134
+ one-line docstring, refactor or split it instead.
135
+ - Comments explain _why_, not _what_.
136
+ - If you feel the need to explain _what_ the code does, refactor it instead.
137
+ - If comment or docstring restates the obvious, remove it.
138
+
139
+ ## Logging
140
+
141
+ - Use the standard `logging` package and create one module-level logger:
142
+ `logger = logging.getLogger(__name__)`.
143
+ - Configure handlers, levels, and `colorlog` formatters only at application entry
144
+ points. Never configure logging in imported modules.
145
+ - Use the format `%(log_color)s%(levelname)-8s%(reset)s %(name)s: %(message)s`;
146
+ prefer a short package-relative module name when configuring the formatter.
147
+ - Keep messages brief, lower-case, and action-oriented:
148
+ `logger.info("parsed 12 formula")`.
149
+ - Use lazy `%s` formatting for values:
150
+ `logger.debug("processing %s", path)`.
151
+ - Log identifiers and concise counts, not complete document contents, prompts, graph
152
+ payloads, secrets, or personal data.
153
+ - Use `DEBUG` for diagnostics, `INFO` for meaningful progress, `WARNING` for handled
154
+ anomalies, and `ERROR` for operations that fail.
155
+ - Use `logger.exception("failed to parse %s", path)` only inside an `except` block
156
+ when the current layer owns reporting or recovery.
157
+ - Do not log an exception and re-raise it unless this layer adds actionable context
158
+ or terminates the operation.
159
+
160
+ ## Testing
161
+
162
+ - Keep tests under `tests/`.
163
+ - Keep tests in Arrange–Act–Assert order, with one behavioral reason to fail per test.
164
+ - Test observable behavior and domain invariants, not private implementation details.
165
+ - Prefer parametrized tests for equivalent cases with different inputs and outputs.
@@ -0,0 +1,108 @@
1
+ # ARCHITECTURE
2
+
3
+ This document describes `latexwalker` library boundaries.
4
+ Keep it current when changing the project architecture.
5
+
6
+ ## System
7
+
8
+ `latexwalker` is a library for parsing LaTeX source into a syntax tree and
9
+ normalizing its formatting. Parsing and normalization are separate concerns:
10
+ the parser produces a lossless view of the source (every node carries the
11
+ exact region it was built from), and the normalizer re-emits it in canonical
12
+ form.
13
+
14
+ ## Project layout
15
+
16
+ ```text
17
+ src/latexwalker/
18
+ ├── __init__.py # curated public API surface
19
+ ├── _version.py # generated by hatch-vcs; do not edit
20
+ ├── errors.py # LatexWalkerError hierarchy and ParseError context
21
+ ├── source.py # SourcePoint / SourceSpan schemas and SourceMap offset index
22
+ ├── tokens.py # TokenType vocabulary and immutable Token schema
23
+ ├── lexer.py # streaming tokenizer over raw text
24
+ ├── nodes.py # frozen syntax tree schemas (Document root + Content union)
25
+ ├── parser.py # recursive-descent parser assembling documents
26
+ ├── traverse.py # structural traversal over any node kind
27
+ ├── rules.py # normalization rule-set schemas and YAML decoding
28
+ ├── zones.py # preserve / math / text classification of source regions
29
+ ├── rewrite.py # rule set fused into a single-pass rewriting automaton
30
+ ├── normalize.py # public canonicalization entry point
31
+ └── resources/
32
+ └── rules.yaml
33
+ ```
34
+
35
+ ## Pipeline
36
+
37
+ 1. **Lexing** (`lexer.tokenize`) converts a `SourceMap`-backed text into a
38
+ stream of `Token`s ending with an end-of-input sentinel. Whitespace runs
39
+ become `WHITESPACE` or, when they contain two or more newlines,
40
+ `PARAGRAPH_BREAK`. Comments run to the end of their line. Macro values
41
+ exclude the leading backslash.
42
+ 2. **Parsing** (`parser.parse_document`) consumes tokens recursively into a
43
+ `Document` of frozen nodes. Each construct (groups, macros with attached
44
+ arguments, environments, math regions) declares the closers it owns, so
45
+ errors point at precise spans via `ParseError`. Nesting beyond a fixed
46
+ depth bound also fails with `ParseError`; the bound is chosen low enough
47
+ that nested parses never approach the interpreter's own stack limit.
48
+ 3. **Traversal** (`traverse.walk`, `traverse.iter_children`) visits trees
49
+ structurally; unknown future node kinds fall through safely.
50
+ 4. **Re-emission** currently means lossless extraction through spans
51
+ (`Document.source_of`); canonical formatting will build on the same node
52
+ contracts.
53
+ 5. **Normalization** (`normalize.normalize`) reduces plain text toward its
54
+ canonical form. `zones.classify` first fences off what must never change
55
+ (comments, verbatim-family environments) and labels math regions; the
56
+ rewriteable zones then pass through `rewrite.CompiledRuleSet`, a rule
57
+ set fused into a single alternation so one regex scan serves any rule
58
+ count. Rule sets are strict msgspec schemas (`rules.RuleSet`) decoded
59
+ from YAML; the packaged `resources/rules.yaml` supplies defaults and
60
+ is the stage's only filesystem access.
61
+
62
+ ## Lexer decisions
63
+
64
+ The lexer keys on raw characters instead of TeX's line-based
65
+ canonicalization. These behaviors are deliberate and pinned by tests:
66
+
67
+ - Control sequences never span lines: scanning stops at the newline, which is
68
+ emitted as its own whitespace token.
69
+ - A backslash followed by LF fails with `ParseError`; a backslash followed by
70
+ CR forms a control-symbol macro whose name is the CR character.
71
+ - Comments extend through any trailing CR of a CRLF terminator before ending
72
+ at the LF.
73
+ - Paragraph breaks are detected by counting raw LF characters (two or more)
74
+ in a whitespace run; blank lines built from CR alone stay whitespace.
75
+
76
+ ## Boundaries
77
+
78
+ ### Core
79
+
80
+ - Tokenizing and parsing raw LaTeX text into a syntax tree.
81
+ - The core does not read from or write to the filesystem.
82
+
83
+ ### Normalization
84
+
85
+ - Works on plain strings, independent of parser node types, so it runs
86
+ before lexing or over extracted spans equally well.
87
+ - Rules are user-authored pattern/canonical pairs; scoping (`any`,
88
+ `math`, `text`) declares where a rewrite is render-equivalent.
89
+ - The packaged default rule set is the package's only filesystem read.
90
+
91
+ ### API
92
+
93
+ - The public surface is whatever `src/latexwalker/__init__.py` exports;
94
+ submodules remain importable but are not covered by stability promises.
95
+ - Callers pass in source text and receive a `Document`; internal node types
96
+ are deliberately exported so downstream code can match on them.
97
+ - All errors derive from `LatexWalkerError`.
98
+
99
+ ## Data contracts
100
+
101
+ - Schemas crossing a system boundary are defined with `msgspec.Struct`s;
102
+ decoding and validation happen at those boundaries only.
103
+ - Every schema class is `frozen=True, kw_only=True`, configured once on the
104
+ shared base classes.
105
+ - Boundary schemas reject unknown keys; the rule-set schema additionally
106
+ requires a `version` of at least 1 and non-empty patterns.
107
+ - Trees are immutable after construction; positions are 1-based lines and
108
+ 0-based columns.