Metadata-Version: 2.4
Name: vcti-escapers
Version: 1.0.0
Summary: Escapers for CSV, Markdown, XML, HTML, JSON and URL, each verified against a real parser
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-escapers
Project-URL: Changelog, https://github.com/vcollab/vcti-python-escapers/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: markdown-it-py; extra == "test"
Requires-Dist: html5lib; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# Escapers

Escapers for CSV, Markdown, XML, HTML, JSON and URL, each verified against a real parser

## Overview

Text that came from somewhere else — a name, a status, a free-text reason —
cannot be dropped into a document as it stands. A comma reshapes a CSV row, a
pipe splits a Markdown table cell, an unescaped quote ends an HTML attribute
early and starts whatever follows it. An escaper takes such a value and
returns one that embeds at a *specific* place in a *specific* format without
changing the document's structure.

There is no general "escaped string": a value is escaped **for** somewhere.
The same name that is safe inside a Markdown code span breaks a CSV row, and
the same value quoted for CSV is meaningless inside an XML attribute. So each
escaper here names the position it is for and is correct there and nowhere
else. Two escapers are never applied to the same value for extra safety;
where a document genuinely contains another, they nest once per layer.

Every escaper accepts any value whose `str()` succeeds — `None` included —
and returns a string that can be written as UTF-8. Each is verified by
round-tripping a hostile value through a real parser for its format, in the
position a consumer would use it, rather than by reading the specification.

## Installation

```bash
pip install vcti-escapers
```

### In `requirements.txt`

```
vcti-escapers>=1.0.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-escapers>=1.0.0",
]
```

---

## Quick Start

```python
from vcti.escapers import csv_field, html_attribute, json_string, xml_text

csv_field("failed, retried")  # '"failed, retried"'
csv_field(138.0)  # '138.0'  — numbers stay bare
xml_text("a & b")  # 'a &amp; b'
html_attribute('" onclick="')  # '&quot; onclick=&quot;'
json_string("</script>")  # '"\\u003c/script\\u003e"'
```

Every escaper takes any value whose `str()` succeeds, `None` included. What
absence looks like is the target's decision, so each escaper documents its
own answer.

```python
csv_field(None)      # ''    — an empty field
json_string(None)    # '""'  — an empty JSON string, since null is not one
```

---

## The escapers

They form one flat pool, grouped here by format for finding rather than
because a format owns them.

| Format | Escaper | For |
|---|---|---|
| Markdown | `markdown_code` | An inline code span in prose — content shown literally, not obeyed |
| Markdown | `markdown_table_cell` | A code span in a GFM table cell, where `\|` must also be escaped |
| CSV | `csv_field` | One RFC 4180 field; text quoted, numbers bare |
| XML | `xml_text` | Character data between tags |
| XML | `xml_attribute` | Inside an attribute (caller supplies the quotes) |
| HTML | `html_text` | Ordinary text content between tags |
| HTML | `html_attribute` | Inside a quoted ordinary attribute (caller supplies the quotes) |
| JSON | `json_string` | A string literal, quotes included, safe inside `<script>` |
| URL | `url_path_segment` | One path segment — a slash stays inside it |
| URL | `url_query_value` | One query parameter value, form-encoded |

A format appears more than once because a format needs different escaping in
different positions — that distinction is the point, and picking the wrong
one is the mistake this package exists to prevent. Read each escaper's
docstring before first use; several carry limits that matter.

Two worth knowing up front:

- **`html_attribute` is for ordinary attributes.** An event handler
  (`onclick`), a URL attribute (`href`, `src`) or `style` needs more than
  escaping — a perfectly escaped `javascript:` URL still runs.
- **Escapers do not chain for extra safety.** They nest, once per layer, when
  a document genuinely contains another — see
  [docs/patterns.md](docs/patterns.md).

---

## Using them with a template engine

Escapers are plain functions, so they register wherever a template engine
takes callables. Nothing here depends on a template engine or knows one
exists.

```python
from vcti.escapers import csv_field, markdown_code

filters = {"csv_field": csv_field, "markdown_code": markdown_code}
# Jinja2:  environment.filters.update(filters)
```

Registered under their own names, they read the same in a template as in
Python — `{{ item.name | csv_field }}`.

---

## Dependencies

None.

---

## Documentation

| If you want to… | Read |
|---|---|
| See practical, real-world usage | [docs/patterns.md](docs/patterns.md) |
| Understand the architecture and design decisions | [docs/design.md](docs/design.md) |
| Navigate and understand the source | [docs/source-guide.md](docs/source-guide.md) |
| Add an escaper | [docs/extending.md](docs/extending.md) |
