A Decoder reads what a server sends (text with ANSI and VT sequences mixed in) and turns it into
MarkupText lines. Styling becomes markup runs, so a decoded line renders, measures, wraps and
serializes like any other MarkupText. Rendering a line for an output is covered in
Display.
using MarkupString;
using MarkupString.Ansi;
using MarkupString.Ansi.Decoding;
using MarkupString.Decoding;
MarkupRegistry.Default = MarkupRegistry.Empty.WithAnsi();
// Once, at startup: which protocols to read.
var registry = DecoderRegistry.Empty.WithVt();
// One per connection: it holds that connection's style, half-read sequences and pending line.
var decoder = registry.CreateDecoder(new DecoderOptions
{
Identity = new ClientIdentity { Name = "MyClient", Version = "2.1", Columns = 100, Rows = 40 },
});
var replies = new List<string>(); // send each of these to the server
decoder.EventRaised += e =>
{
switch (e)
{
case ReplyEvent reply: replies.Add(reply.Text); break;
case TitleEvent title: Console.WriteLine($"title: {title.Title}"); break;
}
};
foreach (var line in decoder.Feed("\e]2;The Hall\a\e[1;33mWelcome\e[0m back.\r\nExits: north\e[c\r\n> "))
{
Console.WriteLine($"{line.Sequence}: {line.Content.Render(MarkupFormat.Ansi)}");
}
// The telnet layer saw GA or EOR.
if (decoder.EndPrompt() is { } prompt) Console.WriteLine($"prompt: {prompt.Content}");title: The Hall
0: \e[1;33mWelcome\e[0m back.
1: Exits: north
prompt: >
Output in this guide shows ESC as \e and BEL as \a. Events are raised while Feed reads,
before it returns the lines. The \e[c on the second line
asked for device attributes, so replies holds \e[?62;22c.
The registry is immutable and shared. A decoder is not safe to use from two threads at once.
Feed returns the lines its text completed, which may be none. A line ends at a line feed in the
text. Whatever is left is the pending line (PendingLine, HasPendingLine), and a sequence cut in
half by a packet boundary is finished by the next Feed:
var decoder = registry.CreateDecoder();
decoder.Feed("\e[3"); // no lines
decoder.Feed("1mred\n"); // \e[31mred\e[0m
decoder.Feed("still red\n"); // \e[31mstill red\e[0mThe style carries from line to line, as it does on a terminal. Each line is rendered closed, so it stands alone in scrollback.
Two calls end a line for the host:
| Call | When | Returns |
|---|---|---|
EndLine() |
Your telnet layer strips line terminators and knows where a line ended | The line, even when empty |
EndPrompt() |
Telnet GA or EOR | The line with Kind = LineKind.Prompt, or null when nothing was read |
Both abandon a sequence still in flight. Decoder.DecodeAll(text, registry) decodes a whole string
at once and includes a last line that has no line break:
foreach (var line in Decoder.DecodeAll("one\ntwo", registry))
Console.WriteLine($"{line.Kind} {line.Content}");Output one
Output two
A DisplayLine carries Content, Kind, Sequence (its number within the decoder, from 0, for
keying a view) and Destination (the pane it was sent to, or null for the main output).
PendingLine is the text read so far on a line that has not ended, as MarkupText, and
OpenElements lists the elements a text protocol has open, innermost last. A host that keeps state
for as long as an element stays open (an MXP room-name flag that spans several lines, say) checks
OpenElements after each line.
Reset() forgets the style, open elements, half-read sequences and the pending line. Set
decoder.Identity again when the pane is resized; answers read it each time.
Anything that is not text to show is raised through EventRaised, as it is found.
| Event | Raised for |
|---|---|
ReplyEvent(Text) |
An answer to a query. Send Text to the server exactly as it is. |
TitleEvent(Title, Icon) |
OSC 0, 1, 2. Icon is true for 0 and 1. |
PaletteEvent(Slot, Index, Color) |
OSC 4, 10, 11, 12 set a colour; 104, 110, 111, 112 reset one (Color null). |
ClipboardEvent(Selection, Text) |
OSC 52. Text is null when the server asked to read the clipboard. |
ProtocolEvent(Name, Active) |
A text protocol was turned on or off. |
VariableEvent(Name, Value) |
A server variable (MXP). |
GaugeEvent(Variable, Maximum, Caption, Color, Status) |
An MXP GAUGE (Status false) or STAT (Status true) over an entity. |
DownloadEvent(Name, Size) |
A file offered to save (iTerm2 inline=0). The decoder never saves it. |
The decoder acts on none of these. Refuse a clipboard write unless the player allowed it.
WithVt() adds five plugins. VtDecoding.Terminal is a registry with just these.
| Codes | Effect |
|---|---|
empty, 0 |
Reset |
1, 2, 3 |
Bold, faint, italic |
4, 21 |
Underline. 4:0 turns it off; 4:1 to 4:5 are all drawn as one underline. |
5, 6 |
Blink |
7, 8, 9 |
Inverse, conceal, strikethrough |
22 |
Neither bold nor faint |
23, 24, 25, 27, 28, 29 |
Turn off italic, underline, blink, inverse, conceal, strikethrough |
30–37, 90–97 |
Standard foreground, bright for 90–97 |
40–47, 100–107 |
Standard background, bright for 100–107 |
38, 48 |
Extended colour: 5;n, 2;r;g;b, and the colon forms 5:n, 2:r:g:b, 2::r:g:b, 2:cs:r:g:b |
39, 49 |
Default foreground, background |
53, 55 |
Overline on, off |
58 |
Underline colour: its parameters are read and dropped |
Any other code is ignored. An extended colour out of range leaves the colour as it was, and so does one
with too few components, whose components are not read as codes of their own. The style
is written under the VtPens.Style pen as an AnsiMarkup.
| Input | Effect |
|---|---|
| BEL | A BellMarkup point |
| CR | Cursor to column 0 |
| BS | Cursor one cell left, stopping at 0 |
| HT | Cursor to the next multiple of 8 (ControlsPlugin.TabStop) |
| FF | A ClearScreenMarkup point |
| VT | Ends the line |
CSI K |
0 erase to end of line, 1 blank to the cursor, 2 erase the line. Other values are not claimed. |
CSI J |
2 or 3 writes a ClearScreenMarkup point. 0 and 1 are claimed and do nothing: a scrolling pane has no screen below the cursor. |
CSI G, CSI ` |
Cursor to column n (from 1) |
CSI C, CSI D |
Cursor n cells right, left |
ESC 7, ESC 8 |
Save the cursor's column, and go back to it |
CSI A, ESC D, ESC M |
Claimed and ignored: a scrolling pane cannot go up a line, or down one without ending it |
A line feed always ends the line, with or without this plugin. DEL is dropped. C1 controls (U+0080 to U+009F) are text, as xterm treats them in UTF-8. CAN and SUB cancel a sequence in flight.
ESC ( 0 and ESC ) 0 designate the DEC special graphics set into G0 or G1; ESC ( B and
ESC ) B put ASCII back. SO and SI shift between G0 and G1. While graphics is in force, _ to ~
become line drawing and symbols:
Decoder.DecodeAll("\e(0lqqk\e(B ok", registry)[0].Content; // ┌──┐ ok| OSC | Effect |
|---|---|
0, 1, 2 |
TitleEvent, control characters removed |
4;n;spec… |
PaletteEvent for each index and colour pair |
104 |
Reset the listed entries, or every entry (Index = -1) when empty |
10, 11, 12 |
Set the foreground, background, cursor colour, or answer ? (10 and 11 only). 10;a;b sets 10 and 11 at once. |
110, 111, 112 |
Reset them |
52;sel;base64 |
ClipboardEvent with the decoded text, or a null text for ?. Invalid base64 is dropped. |
8;params;uri |
A link over the text that follows, until an empty uri. An address longer than 2083 characters, or one DecoderOptions.Links refuses (UrlPolicy.Safe unless set, which refuses javascript: and the like), ends the link instead. An id= among the params becomes a LinkGroupMarkup, and so Interaction.Group: the text under it is one link even across lines. |
68;1;SEND;command |
An MSLP command link over the underlined text that follows. Turning the underline off ends it, and so does the end of the line. |
Colours are X11 specs: rgb:r/g/b with one to four hex digits a channel, or #rgb, #rrggbb,
#rrrgggbbb, #rrrrggggbbbb. OscPlugin.ParseColor and FormatColor are public. Links are
written under the VtPens.Link pen, an OSC 8 id under VtPens.LinkGroup.
| Query | Reply | Allowed by |
|---|---|---|
CSI c, CSI 0 c |
\e[?62;22c, or \e[?62;4;22c when Identity.Output.Pictures has Sixel |
Identity |
CSI > q |
\eP>|Name Version\e\\ (XTVERSION) |
Identity |
CSI 5 n |
\e[0n |
Identity |
CSI 14 t, 16 t, 18 t |
The pane in pixels, a cell in pixels, the pane in cells | Size |
OSC 10 ; ?, OSC 11 ; ? |
Identity.Foreground, Background as rgb:rrrr/gggg/bbbb, when set |
Colors |
CSI 6 n |
\e[{Rows};{column + 1}R |
CursorPosition |
A query whose answer is not allowed is still claimed, and nothing is sent.
A carriage return or backspace moves the cursor back, and what is written next overwrites cells in place. A progress bar redrawn over itself ends as its last state.
| Input | Line |
|---|---|
Loading 10%\rLoading 100% |
Loading 100% |
abcdef\rXY |
XYcdef |
abc\b\bX |
aXc |
hello world\r\e[Kbye |
bye |
a\tb |
a b |
a\e[10Gb |
a b |
漢字\rX |
X 字 |
Moving the cursor past the end of the line pads with spaces only when text follows, so a tab at the end of a line leaves no trailing blanks. The style of overwritten cells is replaced with the new text's. A wide character that is only partly overwritten is blanked where it is not covered, as a terminal blanks it.
Every buffer has a cap. VtLimits holds the tokenizer's:
| Property | Default | |
|---|---|---|
MaxParameters |
32 | Parameters in a CSI or DCS header, subparameters included |
MaxParameterValue |
65535 | Larger values are clamped |
MaxStringLength |
16 MiB | An OSC or DCS payload |
MaxApcLength |
64 KiB | An APC payload |
A sequence that overflows is read to its end and dropped. A line feed ends any sequence in flight,
so a bare ESC ] typed into a channel cannot swallow the rest of the session.
DecoderOptions holds the decoder's:
| Property | Default | |
|---|---|---|
MaxOpenElements |
64 | Elements a text protocol may have open at once |
MaxLineLength |
65536 | Characters in a line. A longer line is ended at the limit and the rest starts the next line. |
Links |
UrlPolicy.Safe |
Which link addresses a server's links may have; any other is read as text |
Meters |
null | The IMeterFactory the diagnostics are reported to; null uses a shared meter |
decoder.Diagnostics counts what was dropped:
var decoder = registry.CreateDecoder();
decoder.Feed("\e[?25lHello\e]0;never closed\nworld\u0001\n");
// AbandonedSequences = 1 the OSC cut off by the line feed
// UnhandledSequences = 1 CSI ? 25 l, which no plugin claims
// UnhandledControls = 1 U+0001LimitsReached counts lines broken at the limit and elements refused. PicturesFailed counts pictures that
could not be decoded. Each count is also reported as the metric markupstring.decoding.dropped on the
MarkupString.Decoding meter, tagged with its kind; see Dependency injection.
DecoderRegistry is an IDecoderFactory, so code that makes decoders can take the interface and a
test or host can give it another.
ClientIdentity is what the decoder answers with: Name, Version, Columns, Rows,
CellWidth and CellHeight (8 by 16 by default), Foreground, Background, and Output, the
OutputCapabilities of where the session is shown.
Answers says which questions get an answer. ClientAnswers.Default is everything except
CursorPosition. A server that sends CSI 6 n assumes a full screen it can address, and a scrolling
pane is not one: the reply always gives the last row. Turn it on for a server you know uses it only
to find the column:
var decoder = registry.CreateDecoder(new DecoderOptions
{
Identity = new ClientIdentity
{
Answers = ClientAnswers.Default | ClientAnswers.CursorPosition,
Background = new RgbColor(0, 0, 0),
},
});
decoder.Feed("\e[c\e[>q\e[18t\e]11;?\a> \e[6n");The replies, in order:
\e[?62;22c
\eP>|MarkupString 1.0\e\
\e[8;24;80t
\e]11;rgb:0000/0000/0000\e\
\e[24;3R
ClientAnswers.None answers nothing.
ToMtts gives the bits for the last step of the telnet TTYPE cycle, from Output:
| Bit | Set when |
|---|---|
Vt100, Utf8 |
Always, but Utf8 is cleared for UnicodeLevel.Ascii |
Ansi |
Colors is Standard or better |
Colors256, OscColorPalette |
Xterm256 or better |
TrueColor |
TrueColor |
Mslp |
Interactions has Command |
ScreenReader, Ssl |
You pass screenReader: true, tls: true |
var identity = new ClientIdentity { Output = OutputCapabilities.Browser };
identity.ToMtts(tls: true); // Ansi, Vt100, Utf8, Colors256, OscColorPalette, TrueColor, Mslp, Ssl
(int)identity.ToMtts(tls: true); // 3375, sent as "MTTS 3375"The default Output is OutputCapabilities.PlainText, which gives only Vt100, Utf8.
A plugin is an IDecoderPlugin: a name and a Register method that adds handlers to a
DecoderBuilder.
sealed class CursorPlugin : IDecoderPlugin
{
public string Name => "cursor";
// A scrolling pane has no cursor to show or hide: claim CSI ? 25 h and CSI ? 25 l so they are not counted.
public void Register(DecoderBuilder builder) => builder
.OnCsi('h', static (in CsiSequence sequence, DecodeContext context) => sequence.Get(0) == 25, prefix: '?')
.OnCsi('l', static (in CsiSequence sequence, DecodeContext context) => sequence.Get(0) == 25, prefix: '?');
}
var registry = DecoderRegistry.Empty.WithVt().With(new CursorPlugin());| Method | Handles |
|---|---|
OnCsi(final, handler, prefix, intermediates) |
A control sequence |
OnEsc(final, handler, intermediates) |
An escape sequence |
OnOsc(number, handler) |
An OSC; the handler gets what follows the number and ; |
OnDcs(final, handler, prefix, intermediates) |
A device control string |
OnApc(handler) |
Every APC, until one handler claims it |
OnControl(c, handler) |
A C0 control |
OnPrint(filter) |
Printable text before it is written; return true when the filter wrote it |
OnLineEnd(action) |
Every line end, after the active text protocol's EndLine |
TextProtocol(name, create) |
A protocol carried in the text, such as MXP |
CsiSequence.Get(index, fallback) reads a parameter, giving fallback when it is missing or empty.
IsSubparameter(index) says whether it followed a :. Is(final, prefix) tests the sequence's name.
registry.With(plugin) replaces a plugin of the same name. Has(name) and Plugins list what is
installed.
For one sequence, the newest handler is tried first: handlers from a plugin added later run before
an earlier plugin's, and within a plugin a later call runs before an earlier one. A handler returns
true to claim the sequence and false to pass it to the next older handler. A sequence nobody claims
is counted in UnhandledSequences.
This lets a plugin change one case of a sequence and leave the rest alone:
sealed class BellCount
{
public int Rung { get; set; }
}
sealed class QuietBellPlugin : IDecoderPlugin
{
public string Name => "quiet-bell";
// Added after WithVt, so it sees each bell before ControlsPlugin does.
public void Register(DecoderBuilder builder) => builder.OnControl('\a', static (_, context) =>
{
var bells = context.State<BellCount>();
bells.Rung++;
return bells.Rung > 1; // true swallows it; false lets ControlsPlugin write the bell
});
}Line end handlers are the exception: they all run, in the order they were added.
The plugin instance is shared by every decoder of the registry, so it holds no state of its own.
context.State<T>() gives this decoder's instance of T, made on first use and cleared by
Reset(). The host can read the same instance with decoder.State<T>().
Text is written under the markup in force, which is built from two things:
- Pens are named slots a protocol sets and replaces:
context.SetPen(slot, markup),Pen(slot), andSetPen(slot, null)to clear. SGR is a pen (VtPens.Style); so is a link (VtPens.Link). - Elements are a stack a tag protocol opens and closes:
Open(markup),Close(markup)with the same instance, which also closes anything opened after it, andCloseTo(count).Openreturns false atMaxOpenElements.
Pens sit inside every element, so an SGR colour inside an element wins, as it does in an MXP client.
Besides Print, a handler can Write text as it is (skipping print filters), Write(text, inner)
with one more layer inside, Point(point), move the cursor (CarriageReturn, Backspace,
MoveToColumn), erase (EraseToEndOfLine, EraseToStartOfLine, EraseLine), EndLine(), set
Destination for the lines that follow, Raise(event) and Reply(text).
A text protocol reads every piece of printable text while it is on, and prints what is literal with
context.Print. Only one is on at a time. A protocol that also implements ISequenceObserver is told
each time an escape sequence arrives while it is on; MXP uses this to spend a pending ESC [ 4 z.
context.Insert(column, text) puts text into the line being read at a display column. This one makes *text* bold:
sealed class StarsPlugin : IDecoderPlugin
{
public string Name => "stars";
public void Register(DecoderBuilder builder) => builder.TextProtocol("stars", () => new StarsProtocol());
}
sealed class StarsProtocol : ITextProtocol
{
private readonly IMarkup _bold = AnsiMarkup.Create(bold: true);
private bool _open;
public void Feed(ReadOnlySpan<char> text, DecodeContext context)
{
while (true)
{
var star = text.IndexOf('*');
if (star < 0)
{
context.Print(text);
return;
}
context.Print(text[..star]);
if (_open) Close(context);
else _open = context.Open(_bold);
text = text[(star + 1)..];
}
}
public void EndLine(DecodeContext context) => Close(context);
public void Reset(DecodeContext context) => Close(context);
private void Close(DecodeContext context)
{
if (_open) context.Close(_bold);
_open = false;
}
}The registry offers the protocol; the decoder makes an instance the first time it is turned on, typically after telnet negotiates it:
var decoder = DecoderRegistry.Empty.WithVt().With(new StarsPlugin()).CreateDecoder();
decoder.EnableProtocol("stars"); // raises ProtocolEvent { Name = stars, Active = True }
decoder.Feed("a *bold \e[31mred\e[0m bold* plain *open\n")[0].Content.Render(MarkupFormat.Ansi);a \e[1mbold \e[31mred\e[0m\e[1m bold\e[0m plain \e[1mopen\e[0m
The SGR reset inside the element ends the red pen, not the bold element. EndLine closed the
element left open, so the next line starts plain. EnableProtocol returns false when the registry
offers no protocol of that name; ActiveProtocol names the one that is on.
WithPictures() adds PicturesPlugin, which reads Kitty graphics (APC G), iTerm2 inline images
(OSC 1337 File=, and MultipartFile=/FilePart=/FileEnd) and sixel (DCS q). Each picture
becomes an ImageMarkup over the cells it covers, one PictureRow per line, so a cell renderer
draws it and the ANSI writer can send it on to another terminal in whatever way that terminal
draws pictures. Kitty to Kitty, sixel to Kitty, and iTerm2 to half blocks all go through the same
markup.
var registry = DecoderRegistry.Empty.WithVt().WithPictures();
var decoder = registry.CreateDecoder(new DecoderOptions
{
Identity = new ClientIdentity { CellWidth = 8, CellHeight = 16, Columns = 100 },
});
var lines = decoder.Feed(wire);
var store = decoder.Pictures(); // this connection's PictureStore
foreach (var image in lines.SelectMany(l => l.Content.Runs).SelectMany(r => r.Markups).OfType<ImageMarkup>())
{
if (store.TryGet(image, out var picture)) Console.WriteLine($"{picture.Width}x{picture.Height} {picture.MediaType}");
}The pixels live in the decoder's PictureStore, and an image's Source is picture: and a key
(a hash of the picture, so the same picture sent twice is stored once). The store:
- keeps its pictures in an
IPictureCache: by default aMemoryPictureCacheof its own, which holdsBudgetbytes (64 MiB) and drops the least recently used first. A line whose picture is gone shows its text instead. - shares one cache between connections with
WithPictures(cache), so a picture every player is sent is decoded and kept once. A file already in the cache is not decoded again. The cache can be your own (on disk, in Redis); it must be safe to call from several threads. A connection that adds frames to a picture does so on a copy under a key of its own, so the still picture other lines show does not change. - refuses a picture of more than
MaxPixels(4096 × 4096). - is the
ITerminalPictureSourceto giveAnsiOutputOptions.Pictures, so decoded pictures can be written to a terminal again. - gives
StoredPicture.ToDataUrl()for an<img>, and raisesAddedso a host can serve pictures from its own address instead. A picture already in a shared cache is not raised again.
var cache = new MemoryPictureCache { Budget = 256L * 1024 * 1024 }; // one for the whole server
var registry = DecoderRegistry.Empty.WithVt().WithPictures(cache);
var decoder = registry.CreateDecoder(); // per connection, same cachePNG and sixel are decoded here, and Kitty's raw RGB and RGBA, zlib-compressed or not. A GIF, JPEG or WebP sent as an iTerm2 image is kept as the file with no pixels: a browser shows it, a terminal gets its text.
A picture's size in cells comes from its pixels and ClientIdentity.CellWidth and CellHeight,
unless the sequence says otherwise (Kitty c and r, iTerm2 width and height in cells, px
or %). It is never wider than ClientIdentity.Columns.
Placement follows the terminal: the first row beside the text already on the line, each further row
on its own line, the cursor below the picture afterwards. A picture drawn between ESC 7 and
ESC 8, or Kitty's C=1, is drawn over the lines that follow instead, which is how a server writes
a picture beside text and how this package's own ANSI writer sends iTerm2 and sixel pictures.
Kitty specifics:
- Only
t=d(the picture in the sequence) is read. A file, temporary file or shared memory is refused withEINVAL, since the server's files are not the client's. a=t,a=T,a=p,a=qanda=dare read, and so are animation frames.a=fadds a frame, or edits framer: built on a copy of framec(or a blank filled withY), its data put atx,yand blended over what is there unlessX=1, shown forzms (40 unless given).a=awithrandzsets a frame's time. Playing, stopping and looping are left to the client, and so is composing (a=c), which is counted as unhandled. At mostPicturesPlugin.MaxFrames(1000) frames are kept. A picture with frames is aStoredPicturewhoseIsMovingis true; itsPixels.Framesgrow as frames arrive, andEncodedandToDataUrl()give it as a looping GIF.- Unicode placeholders (U+10EEEE with row and column diacritics, the image id in the foreground
colour) draw a picture transmitted with
U=1. That is how this package's writer sends Kitty pictures, so its output reads back cell for cell. - Replies (
\e_Gi=…;OK\e\\) followqand needClientAnswers.Pictures. A picture sent with neitherinorIgets none.
An iTerm2 file with inline=0 is not shown: a DownloadEvent says it was offered. Nothing is saved.
MarkupString.Mxp adds WithMxp() for a DecoderRegistry. MXP is a text protocol, off until the
host turns it on after telnet negotiates option 91:
using MarkupString.Mxp.Decoding;
var registry = DecoderRegistry.Empty.WithVt().WithMxp(new MxpOptions { ShowRefusedTags = true });
var decoder = registry.CreateDecoder();
decoder.EnableProtocol(MxpDecoding.Protocol);
decoder.Feed("\e[1z<SEND href=\"buy sword|look sword\" hint=\"Sword|Buy|Look\">sword</SEND>\n");Line modes (ESC [ n z) decide what a line may use. Until MXP is on they are claimed and ignored.
| Mode | |
|---|---|
0, 1, 2 |
This line is open, secure or locked |
3 |
Close every MXP element and go back to open |
4 |
The next tag is secure. Any text, entity or escape sequence before it spends this; a second ESC [ 4 z arms it again. |
5, 6, 7 |
Open, secure or locked from now on |
10, 11, 12, 19 |
This line is a room name, description, exits or welcome text: an MxpFlagMarkup over it |
20–99 |
This line is the element the server tied to that number with TAG= |
Any other number in 10–99, and ESC [ z with no number, changes nothing. Leaving open mode in the
middle of a line (modes 1, 2, 6, 7 or a line tag) closes the elements the open part opened.
On an open line only the open tags work: B, I, U, S, H, C/COLOR, FONT, BR, SBR,
P, NOBR. A secure tag there is dropped and counted in UnhandledSequences. On a locked line <
and & are text. Elements opened on an open line close at its end, and A and SEND always end
with their line.
MxpOptions changes two things:
| Property | Default | |
|---|---|---|
ShowRefusedTags |
false | Show a refused tag (a secure tag or a definition on an open line, the close of a secure tag nothing opened) as the text it was, instead of dropping it |
Supports |
null | The tags and attributes <SUPPORT> is answered with, such as { ["send"] = ["href", "hint"] }. Null answers with every tag the package reads. Answer(asked) gives the same answer to a host that answers MXP support itself. |
What the tags become:
| Tag | Markup |
|---|---|
B, I, U, S, H, COLOR, FONT |
An AnsiMarkup. SGR inside an MXP colour wins, as in an MXP client. |
A href hint |
A URL link, when DecoderOptions.Links allows the address |
SEND href hint prompt |
A command link. With several commands (a|b) or PROMPT, a CommandMenuMarkup as well. With no href, or &text; in it, the command is the tag's text. One command takes the whole hint. With several, one more hint than commands makes the first the link's hint and the rest the menu labels; as many hints as commands are the labels alone. |
IMAGE fname url t w h align |
An ImageMarkup at url, then the t directory, then fname. Sized in cells (W=4c H=2c), it is drawn over the cells the server leaves blank for it; when those cells hold text, it goes inline at the tag instead. Sized in cells one way only, the other size is converted to pixels with the identity's cell size. |
SOUND, MUSIC |
SoundMarkup (with Priority from P and Type from T) or SoundStopMarkup points, at u, then t, then the file |
FRAME, DEST |
A PaneMarkup over the text in DEST, titled from its FRAME. FRAME ACTION=REDIRECT (or the REDIRECT flag) sends the lines after it to that frame through DisplayLine.Destination, until a redirect to _top or _previous or FRAME … CLOSE. |
GAUGE, STAT |
A GaugeEvent |
VAR name |
A VariableMarkup over its text, and a VariableEvent |
EXPIRE, RELOCATE, USER, PASSWORD |
Their points |
VERSION, SUPPORT |
Answered (\e[1z<VERSION …>, \e[1z<SUPPORTS +send …>) when ClientAnswers.Mxp allows. <VERSION n> from the server sets the STYLE the answer reports. |
<!ELEMENT> defines an element from other tags, with ATT attributes substituted for &name;,
FLAG for an MxpFlagMarkup, OPEN, EMPTY and TAG. <!ENTITY> defines an entity, used as
&name; and published as a VariableEvent unless PRIVATE. <!ATTLIST> and <!TAG> are read
too. To serialise lines with MxpFlagMarkup, add MxpDecoding.FlagCodec to the MarkupRegistry.
An element defined on an open line plays back as open text, so a server cannot launder a secure tag
through one; it plays back secure only when used on a secure line or by a line tag. Definitions are
bounded by MxpPlugin constants:
| Constant | Value | |
|---|---|---|
MaxElements |
256 | Elements defined at once |
MaxEntities |
512 | Entities defined at once |
MaxDefinitionLength |
1024 | Characters in an element's definition or an entity's value, <VAR> included |
MaxExpansionDepth |
4 | Elements expanding inside each other (an element that uses itself stops here) |
MaxExpansionsPerLine |
64 | Custom elements expanded on one line |
A cell size (W=Nc, H=Nc) over 1024 is ignored, so the picture goes inline. A definition past a limit is refused and counted in LimitsReached. The built-in tags and the entities
the decoder already knows (lt, nbsp and the rest) cannot be redefined. <!ENTITY name value REMOVE> takes value
out of a | list, and DELETE deletes the entity.
What MarkupFormat.Mxp writes reads back to the same markup.
MarkupString.Pueblo adds WithPueblo(). Turn the protocol on when the server greets the client
(PuebloDecoding.IsGreeting(line)) and the client has answered PUEBLOCLIENT.
The server starts in MUD text, where tags are text. </xch_mudtext> or <img xch_mode=html>
switches to HTML, where a line ends at <br> and a bare line feed is whitespace; <xch_mudtext>
switches back, and its region is a PreformattedMarkup. ANSI is read in both.
| Tag | Markup |
|---|---|
b, i, u, s, font color |
An AnsiMarkup |
a xch_cmd xch_hint, a href |
A command or URL link. xch_mode=prompt puts the command on the input line, as a CommandMenuMarkup with Prompt. |
send href |
A command link |
img src alt |
An ImageMarkup over the alt text |
img xch_sound=play|loop|stop href xch_volume |
SoundMarkup or SoundStopMarkup |
xch_pane action=redirect name panetitle |
A PaneMarkup until the next redirect |
xch_page clear=text, xch_prefetch href |
ClearScreenMarkup, PrefetchMarkup |
br, p, hr, pre |
Line breaks, and a PreformattedMarkup for pre |
strong and em close b and i, and strike and del close s. Links end with their line.
What MarkupFormat.Pueblo writes reads back to the same markup.