Skip to content

Commit

Permalink
Update markup to match MF2 spec, and include spec text
Browse files Browse the repository at this point in the history
  • Loading branch information
eemeli committed Jan 10, 2024
1 parent 5ef43ac commit 345bf6c
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 61 deletions.
111 changes: 52 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -388,18 +388,62 @@ An expression may have one of three forms:
- An operand with an annotation.
- An annotation with no operand.

There are three different types of supported annotations,
distinguished by their first character:

- `':'` for standalone annotations like numbers and strings,
- `'+'` for "open" annotations that start a markup span, and
- `'-'` for "close" annotations that end a markup span.

The resolution of standalone annotations is customisable
The resolution of annotations using the `:` prefix is customisable
using the constructor's `functions` option,
which takes `MessageFunction` function values that are applied when
the annotation's name (without the `:`) corresponds to the `functions` key.

#### Markup

In addition to expressions, placeholders may also be markup;
content corresponding to HTML elements or other markup syntax.
Unlike expressions, markup does not accept a positional input argument
and its resolution is not customizable by the `functions` option.

Markup placeholders take three different forms:

- "standalone" markup for non-textual content such as inline images,
- "open" markup that starts a markup span, and
- "close" markup that end a markup span.

The syntax used by markup corresponds to that of XML,
though with `#` as a prefix for "standalone" and "open": `{#img /}`, `{#b}`, `{/b}`.

Markup placeholders are not required to be paired or nest cleanly;
within the formatter each is only considered by itself,
and any higher-level validation is the responsibility of the caller.

A markup placeholder cannot be used as a selector.
In `format()`, all markup is effectively ignored, each formatting to an empty string.
In `formatToParts()`, each markup placeholder formats to one part:

```ts
interface MessageMarkupPart {
type: 'markup';
kind: 'open' | 'standalone' | 'close';
source: string;
name: string;
options?: { [key: string]: unknown };
}
```

The `type` of the part is always `"markup"`,
and its `kind` is one of `"open"`, `"standalone"`, or `"close"`.
The `name` matches the name of the markup,
without the `#` or `/` prefixes or suffixes.
The `source` matches the `name` of the markup placeholder,
prefixed and suffixed with the appropriate `#` and `/` characters.

The `options` correspond to the resolved literal and variable values
of the options included in the placeholder.
For example, when formatting `{#open foo=42 bar=$baz}` with `formatToParts({ baz: 13 })`,
the formatted part's `options` would be `{ foo: '42', bar: 13 }`.
For options with variable reference values,
if the resolved value is an object with a `valueOf()` method that does not return the object itself,
the returned value is used.
The `options` are never included for a "close" markup placeholder,
as they are only supported for "open" and "standalone".

### MessageFunction

Fundamentally, messages are formed by concatenating values together.
Expand Down Expand Up @@ -609,57 +653,6 @@ When a `MessageString` is used as a selector,
the returned array may include at most one entry,
if one of the keys was an exact string match for the value.

### Message Markup

In addition to standalone annotations such as `:number` and `:string`,
MF2 messages may include placeholders that indicate the start/open (`+`) and end/close (`-`) of markup spans,
such as `+b` or `-link`.
Markup placeholders are not required to be paired or nest cleanly;
within the formatter each is only considered by itself,
and any higher-level validation is the responsibility of the caller.

Unlike standalone annotations,
the resolution of these placeholders cannot be customised;
they are instead included almost directly in the formatted-parts output.
A markup placeholder always resolves as:

```ts
interface MessageMarkup {
type: 'open' | 'close';
locale: string;
source: string;
name: string;
options: { [key: string]: unknown };
toParts(): [MessageMarkupPart];
toString(): '';
valueOf?: () => unknown;
}

interface MessageMarkupPart {
type: 'open' | 'close';
source: string;
name: string;
value?: unknown;
options: { [key: string]: unknown };
}
```

A markup placeholder cannot be used as a selector.
When formatted to a string, a markup placeholder is formatted as an empty string.
The `name` of the `MessageMarkup` matches the name of the annotation,
without the `+` or `-` prefix.
If the placeholder includes an input argument,
the `valueOf()` method is defined and returns that value,
and the `source` matches that of the input argument.
Otherwise, the `source` matches the `name` of the markup placeholder,
prefixed with the appropriate `+` or `-` character.

When `MessageMarkup` is formatted to parts,
the `type`, `name` and `source` of the `MessageMarkupPart` match those of the `MessageMarkup`.
For each of the `value` and `options` values,
if the corresponding value is an object with a `valueOf()` method that does not return the object itself,
the returned value is used.

### Fallback Values

It's possible for a `MessageFunction` call to throw an error,
Expand Down
103 changes: 101 additions & 2 deletions spec.emu
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,49 @@ contributors: Eemeli Aro
<emu-clause id="sec-intl-messageformat-abstracts">
<h1>Abstract Operations for MessageFormat Objects</h1>

<emu-clause id="sec-formatmarkuppart" type="abstract operation">
<h1>
FormatMarkupPart (
_mf_: an Object,
_values_: an Object or undefined,
_onError_: a Function or undefined,
_markup_: an Object,
): an Object
</h1>
<dl class="header">
<dt>description</dt>
<dd>It resolves the formatted part value of a markup placeholder.</dd>
</dl>

<emu-alg>
1. Let _kind_ be ? Get(_markup_, *"kind"*).
1. Let _name_ be ? Get(_markup_, *"name"*).
1. Let _options_ be ? Get(_markup_, *"options"*).
1. Let _part_ be OrdinaryObjectCreate(%Object.prototype%).
1. Perform ! CreateDataPropertyOrThrow(_part_, *"type"*, *"markup"*).
1. Perform ! CreateDataPropertyOrThrow(_part_, *"kind"*, _kind_).
1. Perform ! CreateDataPropertyOrThrow(_part_, *"name"*, _name_).
1. If _kind_ is not *"close"* and _options_ is not *undefined*, then
1. Let _len_ be ? LengthOfArrayLike(_options_).
1. If _len_ > 0, then
1. Let _opts_ be ? CreateListFromArrayLike(_options_).
1. Let _resOptions_ be OrdinaryObjectCreate(%Object.prototype%).
1. For each element _opt_ of _opts_,
1. Let _optName_ be ? Get(_opt_, *"name"*).
1. Let _optValue_ be ? Get(_opt_, *"value"*).
1. Let _resValue_ be ResolveValue(_mf_, _values_, _onError_, _optValue_).
1. If _resValue_ is an Object, then
1. Let _valueOf_ be ? Get(_resValue_, *"valueOf"*).
1. If IsCallable(_valueOf_) is *true*, then
1. Let _rv_ be ? Call(_valueOf_, _resValue_).
1. If IsStrictlyEqual(_rv_, _resValue_) is *false*, then
1. Set _resValue_ to _rv_.
1. Perform ! CreateDataPropertyOrThrow(_resOptions_, _optName_, _resValue_).
1. Perform ! CreateDataPropertyOrThrow(_part_, *"options"*, _resOptions_).
1. Return CreateArrayFromList(« _part_ »).
</emu-alg>
</emu-clause>

<emu-clause id="sec-getmessagedata" type="implementation-defined abstract operation">
<h1>
GetMessageData (
Expand Down Expand Up @@ -416,6 +459,30 @@ contributors: Eemeli Aro
</emu-alg>
</emu-clause>

<emu-clause id="sec-resolvemarkup" type="abstract operation">
<h1>
ResolveMarkup (
_mf_: an Object,
_values_: an Object or undefined,
_onError_: a Function or undefined,
_markup_: an Object,
): an Object
</h1>
<dl class="header">
<dt>description</dt>
<dd>It resolves the value of a markup placeholder for formatting.</dd>
</dl>

<emu-alg>
1. Let _toParts_ be a function that returns FormatMarkupPart(_mf_, _values_, _onError_, _el_).
1. Let _toString_ be a function that returns an empty String.
1. Let _mv_ be OrdinaryObjectCreate(%Object.prototype%).
1. Perform ! CreateDataPropertyOrThrow(_mv_, *"toParts"*, _toParts_).
1. Perform ! CreateDataPropertyOrThrow(_mv_, *"toString"*, _toString_).
1. Return _mv_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-resolvemessage" type="abstract operation">
<h1>
ResolveMessage (
Expand All @@ -426,7 +493,7 @@ contributors: Eemeli Aro
</h1>
<dl class="header">
<dt>description</dt>
<dd>It resolves a message during formatting into a List of MessageValue Objects.</dd>
<dd>It resolves a message during formatting into a List of Strings and MessageValue Objects.</dd>
</dl>

<emu-alg>
Expand All @@ -436,12 +503,44 @@ contributors: Eemeli Aro
1. If _el_ is a String, then
1. Append _el_ to _result_.
1. Else,
1. Let _mv_ be ResolveExpression(_mf_, _values_, _onError_, _el_).
1. Let _elType_ be ? Get(_el_, *"type"*).
1. If _elType_ is *"markup"*, then
1. Let _mv_ be ResolveMarkup(_mf_, _values_, _onError_, _el_).
1. Else,
1. Assert: _elType_ is *"expression"*.
1. Let _mv_ be ResolveExpression(_mf_, _values_, _onError_, _el_).
1. Append _mv_ to _result_.
1. Return _result_.
</emu-alg>
</emu-clause>

<emu-clause id="sec-resolvevalue" type="abstract operation">
<h1>
ResolveValue (
_mf_: an Object,
_values_: an Object or undefined,
_onError_: a Function or undefined,
_value_: an Object,
): an ECMAScript language value
</h1>
<dl class="header">
<dt>description</dt>
<dd>
It resolves the value of a literal or variable reference.
For literals, the returned value is always a String.
For variable references, the returned value may be any ECMAScript language value.
</dd>
</dl>

<emu-alg>
1. Let _type_ be ? Get(_value_, *"type"*).
1. If _type_ is *"literal"*, return ? Get(_value_, *"value"*).
1. Assert: _type_ is *"variable"*.
1. Let _name_ be ? Get(_value_, *"name"*).
1. ...TODO
</emu-alg>
</emu-clause>

<emu-clause id="sec-selectpattern" type="abstract operation">
<h1>
SelectPattern (
Expand Down

0 comments on commit 345bf6c

Please sign in to comment.