Reference

Escape sequences

How a value carries characters that would otherwise be read as structure — and, just as importantly, which sequences are deliberately left alone.

The shape

A sequence is the message’s escape character, a body, and the escape character again.

\F\        \X0D\        \.br\        \Z0102\
^ ^        ^    ^
| body     | body
escape     escape

The escape character is whatever MSH-2 position 3 declared — conventionally \, but a message using ? writes ?F?. Every function takes the message’s delimiter set for exactly that reason.

The full table

SequenceMeaningDecoded?
\F\the field separator as datayes
\S\the component separator as datayes
\T\the subcomponent separator as datayes
\R\the repetition separator as datayes
\E\the escape character as datayes
\Xdd..\hexadecimal data; pairs of digits, each pair one byteyes
\H\start highlightingno — kept literally
\N\normal text, ending highlightingno — kept literally
\Zdd..\locally defined, agreed between the two endsno — kept literally
\Cxxyy\switch to a single-byte character setno — kept literally
\Mxxyyzz\switch to a multi-byte character setno — kept literally
\.br\, \.sp 2\, …formatted-text display commandsno — kept literally

Display commands

Used inside formatted-text (FT) fields.

CommandMeaning
.sp <n>end the line and skip n vertical spaces
.brbegin a new output line
.fibegin word wrap (the default)
.nfbegin no-wrap
.in <n>indent by n spaces
.ti <n>temporarily indent n spaces
.sk <n>skip n spaces to the right
.cecentre the next line

Why half are kept literally

The sequences marked “no” say something about presentation or encoding that a plain string cannot carry. There are three options and only one of them is honest.

Drop them

Loses information the sender sent.

Guess at them

Invents information the sender did not send.

Keep them as written

The caller sees exactly what arrived. A caller who understands display commands can act on them; a caller who does not sees text that is at worst ugly rather than wrong.

What the crate does

Decoding

use er7::{Separators, escape::unescape};

let separators = Separators::default();

// Sequences that stand for characters decode.
assert_eq!(unescape(r"Smith \T\ Jones", &separators), "Smith & Jones");
assert_eq!(unescape(r"a\F\b", &separators), "a|b");
assert_eq!(unescape(r"\X0D\", &separators), "\r");
assert_eq!(unescape(r"\X4142\", &separators), "AB");

// Everything else is kept exactly as written, and not damaged.
assert_eq!(unescape(r"line\.br\next", &separators), r"line\.br\next");
assert_eq!(unescape(r"\H\loud\N\", &separators), r"\H\loud\N\");
assert_eq!(unescape(r"a\Fb", &separators), r"a\Fb");   // unterminated

\X..\ decodes only when its body is whole pairs of ASCII hexadecimal digits; \XZZ\ and \X123\ are kept literally, because a body that is not hex was not hex data. Bytes are read as UTF-8 with the usual lossy replacement, which is the best a receiver can do for a sender that meant some other repertoire.

unescape returns a borrowed string when the text contains no escape character at all — the overwhelmingly common case, and free.

Encoding

use er7::escape::escape;

assert_eq!(escape("Smith & Jones", &separators), r"Smith \T\ Jones");
assert_eq!(escape("a|b^c~d&e", &separators), r"a\F\b\S\c\R\d\T\e");
assert_eq!(escape(r"a\b", &separators), r"a\E\b");
assert_eq!(escape("line\r\nnext", &separators), r"line\X0D\\X0A\next");

Two details worth knowing:

  • The escape character is encoded first, so a value containing it is encoded once, not twice.
  • \r and \n become \X0D\ and \X0A\. A literal carriage return in a value would end the segment and truncate the message — the one corruption an ER7 writer must never commit.

The truncation character (#, HL7 v2.7) is not encoded: it is structural only inside MSH-2, so a # in a value is just a #.

Always encode when you write. Subcomponent::set does it for you. Assigning the raw text directly with an unescaped & silently splits the component the next time the message is parsed, shifting every value after it.

The tokenizer

escapes is the layer both functions above are built on. It turns text into a stream of classified tokens, which is what you want when you need more than “decode or don’t”.

use er7::escape::{escapes, Escape};

let tokens: Vec<_> = escapes(r"Dr\S\Who\.br\", &separators).collect();

assert_eq!(tokens, vec![
    Escape::Text("Dr"),
    Escape::Component,
    Escape::Text("Who"),
    Escape::Formatting("br"),
]);

Two properties make it usable as a foundation:

  • It never fails. Text that does not form a valid sequence comes back as Unknown or Unterminated, not an error.
  • It is lossless. Writing every token back reproduces the input exactly.

The crate will not render \.br\ for you — that is a presentation concern and out of scope — but the token stream gives you everything needed to do it yourself.

One known divergence

HL7 scopes escaping to ST, TX, and FT fields and to the fourth component of ED. This crate decodes sequences wherever they appear, because applying the standard’s scope requires knowing each field’s data type, and that requires a dictionary this crate deliberately does not have.

The risk is a false positive on a value that legitimately contains a backslash in a field where escaping does not apply. The mitigations: unrecognized sequences stay literal, so such a value usually round-trips unchanged anyway; and the raw text is always available, so a caller who knows the data type can override.