Gå til hovedinnhold

Implementing a New Format

A format is a reader that turns a file into Blocks and a writer that writes them back. You implement both, carry inline markup as runs, and prove the roundtrip with tests before you register the format.

Structure​

Create a package under core/formats/ with these files:

core/formats/myformat/
├── reader.go # DataFormatReader implementation
├── writer.go # DataFormatWriter implementation
├── config.go # Format-specific configuration
├── reader_test.go # Extraction and roundtrip tests
└── testdata/ # Sample files for testing

Reader​

The reader must implement format.DataFormatReader. Embed format.BaseFormatReader for shared behavior:

package myformat

import (
"context"
"github.com/neokapi/neokapi/core/format"
"github.com/neokapi/neokapi/core/model"
)

type Reader struct {
format.BaseFormatReader
}

func NewReader() *Reader {
return &Reader{
BaseFormatReader: format.BaseFormatReader{
FormatName: "myformat",
FormatDisplayName: "My Format",
FormatMimeType: "application/x-myformat",
FormatExtensions: []string{".myf"},
},
}
}

func (r *Reader) Signature() format.FormatSignature {
return format.FormatSignature{
MIMETypes: []string{"application/x-myformat"},
Extensions: []string{".myf"},
}
}

func (r *Reader) Open(ctx context.Context, doc *model.RawDocument) error {
if doc == nil || doc.Reader == nil {
return fmt.Errorf("myformat: nil document or reader")
}
r.Doc = doc
return nil
}

func (r *Reader) Read(ctx context.Context) <-chan model.PartResult {
ch := make(chan model.PartResult, 64)
go func() {
defer close(ch)

// 1. Emit PartLayerStart
ch <- model.PartResult{Part: &model.Part{
Type: model.PartLayerStart,
Resource: &model.Layer{ID: "doc1", Format: "myformat"},
}}

// 2. Emit Blocks for translatable content
ch <- model.PartResult{Part: &model.Part{
Type: model.PartBlock,
Resource: model.NewBlock("b1", "Hello"),
}}

// 3. Emit PartLayerEnd
ch <- model.PartResult{Part: &model.Part{
Type: model.PartLayerEnd,
Resource: &model.Layer{ID: "doc1", Format: "myformat"},
}}
}()
return ch
}

func (r *Reader) Close() error {
if r.Doc != nil && r.Doc.Reader != nil {
return r.Doc.Reader.Close()
}
return nil
}

The example above emits plain text. Most real-world formats contain inline markup (bold, links, images) that must be preserved through the pipeline; see Inline Code Handling below.

Writer​

The writer must implement format.DataFormatWriter. Embed format.BaseFormatWriter:

type Writer struct {
format.BaseFormatWriter
}

func NewWriter() *Writer {
return &Writer{
BaseFormatWriter: format.BaseFormatWriter{FormatName: "myformat"},
}
}

func (w *Writer) Write(ctx context.Context, parts <-chan *model.Part) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
case part, ok := <-parts:
if !ok {
return nil
}
switch part.Type {
case model.PartBlock:
block := part.Resource.(*model.Block)
// Write translated content: see renderRuns below
case model.PartData:
// Write structural content verbatim
}
}
}
}

Adding and Removing Blocks​

A writer that can add a block to a document, or remove one with its markup, implements format.StructureEditor: Structural() lists insert_block and delete_block, and EditStructure(doc, edits) returns the document with each edit made and every other byte as it was. It suits a format whose block shell is small, such as a catalog's key and value. Structural() answers for the writer's configuration: under one that makes other markup part of a block's shell, such as a note read from the member beside it, it lists nothing. A new block goes where the reader would look for its key, and a comment the reader reads as a block's note goes with the block. Each declaration needs passing cells in the structural operations matrix (core/formats/opsmatrix_structural_test.go), and the format system describes what the JSON, YAML and ARB writers write.


Inline Code Handling​

Most document formats contain inline markup: bold, italic, links, images, line breaks, variables, placeholders. The framework must preserve this markup through the entire pipeline (extraction, content-memory lookup, MT, AI translation, checks, reconstruction) without corruption.

neokapi solves this with the Run model: a block's content is a flat []model.Run sequence. Text travels as TextRuns; inline markup becomes inline-code runs (PcOpen/PcClose for paired tags, Ph for self-closing tokens) that carry the original markup in a Data field. This lets tools, translation engines, and content-memory matching project the runs to plain text, and the writer reconstructs the original markup by re-emitting each run's Data.

The Run Model​

A Run is a discriminated union: exactly one of its pointer fields is set:

type Run struct {
Text *TextRun // plain text chunk
Ph *PlaceholderRun // self-closing: variable, icon, <br>, redaction
PcOpen *PcOpenRun // opening half of a paired code (<a>, <b>, …)
PcClose *PcCloseRun // closing half of a paired code (</a>, </b>, …)
Sub *SubRun // reference to a nested Block (subfilter output)
Plural *PluralRun // ICU plural with per-form Runs
Select *SelectRun // ICU select with per-case Runs
}

The three inline-code runs you reach for most are:

// PlaceholderRun: a self-closing token (<br/>, {count}, an icon).
type PlaceholderRun struct {
ID string // unique within the run sequence
Type string // semantic type (e.g., "fmt:linebreak", "var")
SubType string // optional refinement
Data string // original markup verbatim (e.g., "<br/>")
Equiv string // plain-text equivalent (e.g., "\n")
Disp string // editor display label (e.g., "[BR]")
Constraints *RunConstraints // deletable / cloneable / reorderable
}

// PcOpenRun: the opening half of a paired code. PcCloseRun mirrors it
// (sharing ID) but omits Disp and Constraints; the close inherits the
// opener's behavior.
type PcOpenRun struct {
ID string
Type string // e.g., "fmt:bold", "fmt:link"
SubType string
Data string // e.g., "<b>", "<a href=\"/help\">"
Equiv string
Disp string
Constraints *RunConstraints
}

RunConstraints is the editing-policy triple:

type RunConstraints struct {
Deletable bool // translator may remove this code
Cloneable bool // translator may duplicate this code
Reorderable bool // this code may move relative to others
}

How It Works​

Consider this HTML paragraph:

<p>Click <b>here</b> for <a href="/help">info</a></p>

The reader extracts the <p> content as a single block whose Runs are:

[
{Text: "Click "},
{PcOpen: {ID: "1", Type: "fmt:bold", Data: "<b>"}},
{Text: "here"},
{PcClose: {ID: "1", Type: "fmt:bold", Data: "</b>"}},
{Text: " for "},
{PcOpen: {ID: "2", Type: "fmt:link", Data: "<a href=\"/help\">"}},
{Text: "info"},
{PcClose: {ID: "2", Type: "fmt:link", Data: "</a>"}},
]

The runs are ordered, and a PcClose shares its ID with the matching PcOpen. This means:

  • block.SourceText() returns "Click here for info" (inline-code runs contribute nothing)
  • block.SourceRuns() contains the PcOpen/PcClose pairs above
  • the second run's PcOpen.Data is "<b>" (the original markup, including attributes)

Tools project the runs to plain text and skip the inline codes. Translation engines get clean text with opaque tokens. The writer re-emits each run's Data to reconstruct the original markup, attributes such as class="emphasis" or href="/help" included.

Three Categories of Inline Elements​

When implementing a format reader, classify each inline element into one of three categories:

CategoryRun kindExamplesPattern
Paired tagsPcOpen + PcClose<b>...</b>, **...**, <a>...</a>Wrap content with two runs (shared ID)
Self-closingPh<br/>, <img>, <hr/>Single run, no children
Block-level(not a run)<p>, <div>, <h1>Boundary for a new Block

The reader decides what is inline vs. block-level. For HTML, this distinction is well-defined. For other formats (Markdown, XLIFF, custom XML), you choose the mapping based on what a translator needs to see as a contiguous unit.

Complete Reader Example with Inline Codes​

Here is how a reader collects inline content from a block-level element into a []model.Run slice. This pattern applies to any format with inline markup:

// collectInlineContent builds a run sequence from all text and inline
// elements inside a block-level container node.
func (r *Reader) collectInlineContent(n *html.Node) []model.Run {
var runs []model.Run
r.collectFromNode(n, &runs)
return runs
}

// appendText coalesces adjacent text so consecutive chunks stay one TextRun.
func appendText(runs *[]model.Run, text string) {
if text == "" {
return
}
if n := len(*runs); n > 0 && (*runs)[n-1].Text != nil {
(*runs)[n-1].Text.Text += text
return
}
*runs = append(*runs, model.Run{Text: &model.TextRun{Text: text}})
}

func (r *Reader) collectFromNode(n *html.Node, runs *[]model.Run) {
for child := n.FirstChild; child != nil; child = child.NextSibling {
switch child.Type {
case html.TextNode:
// Plain text: coalesce into the run sequence
appendText(runs, child.Data)

case html.ElementNode:
if selfClosingElements[child.DataAtom] {
// Self-closing: <br/>, <img>, etc. → a Ph run
*runs = append(*runs, model.Run{Ph: &model.PlaceholderRun{
ID: r.nextID(),
Type: child.Data,
Data: renderTag(child), // e.g., "<br/>"
}})
} else if isInlineElement(child) {
// Paired inline: <b>, <a>, <em>, etc.
id := r.nextID()
*runs = append(*runs, model.Run{PcOpen: &model.PcOpenRun{
ID: id,
Type: child.Data,
Data: renderOpenTag(child), // e.g., "<a href=\"/help\">"
}})
r.collectFromNode(child, runs) // Recurse into children
*runs = append(*runs, model.Run{PcClose: &model.PcCloseRun{
ID: id, // shares ID with its PcOpen
Type: child.Data,
Data: fmt.Sprintf("</%s>", child.Data),
}})
}
// Block-level elements are NOT collected; they form new Blocks
}
}
}

Recurse into inline children to handle nested formatting like <b><i>bold italic</i></b>. Each level of nesting appends its own PcOpen/PcClose pair, and the flat run sequence keeps them in order. Attach the collected runs to a block with block.SetSourceRuns(runs), or build the block directly with model.NewRunsBlock(id, runs).

Reconstructing Markup in a Writer​

The writer walks the run sequence and emits each run's content: the text of a TextRun, and the captured Data of an inline-code run. The framework provides model.RenderRunsWithData, which emits both verbatim:

func (w *Writer) renderRuns(buf *strings.Builder, runs []model.Run) {
// RenderRunsWithData emits TextRun content verbatim and re-emits the
// captured Data for every inline-code run (Ph, PcOpen, PcClose, Sub).
buf.WriteString(model.RenderRunsWithData(runs))
}

Replaying Data keeps inline markup faithful: an <a href="/help" class="nav"> tag roundtrips as exactly that string, attributes and all, without the writer understanding it.

A TextRun written by a translation, a pseudo-translation, or an edit made through kapi apply, ksed or MCP apply_edits holds text: inline codes travel as their own runs, so a < or & in one is a character. A writer for a format with markup or escapes encodes those runs. A block an edit rewrote says so: tools replace a source through model.Block.EditSourceRuns, and Block.SourceAsRead returns the source the reader produced and whether the current one differs. Write a block no edit changed as it was read, so the document keeps its bytes, and encode an edited one. model.RenderRunsWith is the run walk RenderRunsWithData uses, with a hook that writes each text run in your format's spelling; the HTML writer uses it to write every < and & of an edited block as a reference.

Some readers leave syntax they do not model in text runs: the Markdown reader keeps backslash escapes and brackets that open no link, the MDX reader keeps expressions, the AsciiDoc reader keeps passthroughs and macros. Such a run is the document's own markup until an edit rewrites it, and then the writer cannot tell markup from wording by the characters alone. format.RenderEditedRuns compares the edited runs with the runs as read: markup an AddedSyntax finds that the block already held, spelled the same, stays, and markup the edit adds is escaped.

Three sweeps in core/formats/escape_symmetry_test.go hold the formats to this. TestEscapeSymmetryOnModify writes a corpus of characters into a translated value, TestEscapeSymmetryOnSourceEdit writes it into an edited source, and TestEditedWordingReadsBackAsText edits each markup format with wording that spells tags, links, expressions and macros, and reads the result back as text. The first two cover every format whose reader and writer share a skeleton, the ZIP containers included; TestEscapeSweepCoversEverySkeletonPair fails when a format gains a skeleton pair outside them.

Attributes and New Codes a Writer Can Write​

Replaying Data keeps a code as it was read. A writer that can also write a changed attribute of a code, or a new code around text, declares it, and the change contract's set_attribute, mark and new codes in a runs payload reach the format only where it does:

// AttrWriter: the attributes a set_attribute may change, per code type.
func (w *Writer) WritableAttrs() map[string][]string {
return map[string][]string{"link:hyperlink": {model.AttrHref}}
}

// Spell the new value into the code's Data, escaped for where it sits, and
// update its Attrs as the reader would read them back. Return the same runs
// in the same order.
func (w *Writer) WriteAttr(seq []model.Run, at int, name, value string) ([]model.Run, error)

// CodeSynthesizer: the vocabulary types the writer writes as a new code.
func (w *Writer) Synthesizes() []string { return []string{"fmt:bold"} }

// Return both halves of a new code as the reader would read them, or an
// error whose message says why the format cannot write one at site.
func (w *Writer) SynthesizeCode(site format.CodeSite) (open, close model.Run, err error)

The change applier calls these when it applies the operation, so the block already holds the bytes the writer writes. Refuse what the format cannot carry with a reason a caller can act on: the Markdown writer refuses a reference link's destination because the definition elsewhere may serve other links. Each declaration needs a cell in the operations matrix (core/formats/opsmatrix_capabilities_test.go) that writes it and reads it back; the build fails on a declaration without one. E-02 lists what each format declares.

Choosing Target vs Source Content​

When writing output, the writer must choose the right content. Use the target runs if a translation exists for the configured locale, otherwise fall back to the source runs:

func (w *Writer) writeBlock(block *model.Block) {
if !w.Locale.IsEmpty() && block.HasTarget(w.Locale) {
// Write translated content (preserving inline codes)
w.renderRuns(buf, block.TargetRuns(w.Locale))
} else {
// Fall back to source
w.renderRuns(buf, block.SourceRuns())
}
}

Using Skeletons for Document Structure​

Block-level structure that surrounds translatable content (opening and closing tags, whitespace, etc.) is captured in a Skeleton. The reader builds a skeleton with text parts and a reference to the block content:

block := model.NewRunsBlock("tu1", runs)
block.Skeleton = &model.Skeleton{
Strategy: model.SkeletonFragmentBased,
Parts: []model.SkeletonPart{
&model.SkeletonText{Text: "<p>"}, // Before content
&model.SkeletonRef{ResourceID: "tu1"}, // Content placeholder
&model.SkeletonText{Text: "</p>\n"}, // After content
},
}

The writer uses the skeleton to reconstruct the document:

if block.Skeleton != nil {
for _, sp := range block.Skeleton.Parts {
switch p := sp.(type) {
case *model.SkeletonText:
fmt.Fprint(w.Output, p.Text) // Emit structure verbatim
case *model.SkeletonRef:
// Emit the translated/source runs with inline codes
w.renderRuns(buf, runs)
}
}
}

The skeleton keeps the block-level structure of the document intact through a roundtrip. A writer without one has to generate the surrounding tags, whitespace and attributes again, and can lose information doing so.


Document encoding​

A reader never decodes a charset. Every caller opens a document through format.OpenDocument, the ingestion seam, which settles the document's encoding against its bytes and hands the reader UTF-8:

  1. A byte-order mark wins. It is evidence in the file: a UTF-8 mark keeps the bytes as they are, a UTF-16 mark selects that decoder, whatever was declared.
  2. A declared encoding other than UTF-8 (--encoding, the recipe's defaults.encoding) selects its decoder from the core/encoding registry.
  3. With neither, the bytes pass through as UTF-8.

On return doc.Encoding names the encoding the document was read as, and the reader copies it onto the root Layer. doc.EncodingSettled reports whether a mark or a declaration settled it. A reader with a charset heuristic of its own, such as the XML prolog or the PO header, applies it only when the document is unsettled; a settled document is UTF-8 whatever its prolog says, and format.XMLCharsetReader(r.Doc) is the encoding/xml CharsetReader that keeps the decoder from refusing the prolog. Keep the prolog in the skeleton as it is: the writer encodes it back in the same charset.

Write-back mirrors the read. SetEncoding on BaseFormatWriter installs the encoder for the run's charset, so a writer serialises UTF-8 and the base encodes the whole output stream. The rule by writer family:

  • A skeleton-based writer and a generative writer both emit text, so the base re-encodes everything they write. A skeleton holds the source's decoded text, so an untouched document is written back byte for byte, prolog included.
  • A byte-faithful writer (a ZIP container, an image, a compiled catalog, or a plugin whose process encodes its own output) declares Binary: true on BaseFormatWriter; its reader declares Binary: true on the FormatSignature. The seam hands such a reader the bytes as they are, and the base applies no charset and no output option to such a writer.
  • A byte-order mark the charset cannot carry keeps the output UTF-8, mark included: the mark is what the next read settles the encoding by.
  • output.encoding in a format's config wins over the run's encoding.

TestDeclaredEncodingSelectsTheCodec and TestDeclaredEncodingRoundTrip in core/formats are the conformance tests: every text format reads an ISO-8859-1, windows-1252 and Shift_JIS document declared as such and writes it back unchanged. A new format joins the sweep through bomSweepFormats.


Run Metadata Fields​

An inline-code run carries the raw markup and several fields beside it. These fields help tools, editors, and checks work with inline codes intelligently:

FieldPurposeExample
discriminatorWhich field is set: PcOpen, PcClose, or Pha PcOpen
TypeSemantic type for tool processing"fmt:bold", "fmt:link", "var"
IDMatches an opening run to its closing run"1" shared by the <b>/</b> pair
DataOriginal markup for roundtrip reconstruction"<a href=\"/help\">"
DispUI label in translation editors"[B]", "[/B]", "[IMG]"
EquivPlain text equivalent"\n" for <br>
ConstraintsEditing policy (Deletable/Cloneable/Reorderable)non-deletable for a {count} variable

Set these fields in the reader when you have the information. At minimum, set the discriminator, Type, ID, and Data. The other fields are optional; editors and tools use them when they are set.


Configuration​

DataFormatConfig requires four methods: FormatName(), Reset(), Validate(), and ApplyMap(values map[string]any) error. ApplyMap applies config values from a map and rejects unknown keys and type mismatches. The format.ApplyMapViaJSON helper (core/format/applymap.go) implements this for struct configs via JSON marshal/unmarshal with DisallowUnknownFields, while configs with complex parsing can hand-write a switch-based ApplyMap (see core/formats/json/config.go). The helper requires the struct's fields to carry matching yaml/json tags for the incoming keys.

type Config struct {
Indent string `yaml:"indent" json:"indent"`
}

func (c *Config) FormatName() string { return "myformat" }
func (c *Config) Reset() { c.Indent = " " }
func (c *Config) Validate() error { return nil }

func (c *Config) ApplyMap(values map[string]any) error {
return format.ApplyMapViaJSON(c, values)
}

Registration​

Add your format inside formats.RegisterAll() in core/formats/register.go. RegisterReader takes the format name, a reader factory, a FormatSignature for detection, and a display name; RegisterWriter takes the name and a writer factory:

// In RegisterAll(reg *registry.FormatRegistry, opts ...RegisterOptions):
reg.RegisterReader("myformat",
func() format.DataFormatReader { return myformat.NewReader() },
format.FormatSignature{
MIMETypes: []string{"application/x-myformat"},
Extensions: []string{".myf"},
}, "My Format")
reg.RegisterWriter("myformat", func() format.DataFormatWriter {
return myformat.NewWriter()
})

Testing​

The helpers used below (testutil.RawDocFromString, RawDocFromReader, CollectBlocks, CollectParts, PartsToChannel) live in github.com/neokapi/neokapi/core/internal/testutil. Being an internal package, it is importable only by formats inside this repository. A plugin format in another repository builds the model.RawDocument and drains the Read channel itself; each helper is a few lines.

Extraction Tests​

Verify that the reader identifies the text and the inline codes:

func TestReadInlineRuns(t *testing.T) {
ctx := context.Background()
reader := NewReader()
err := reader.Open(ctx, testutil.RawDocFromString(
`<html><body><p>Click <b>here</b> for info</p></body></html>`,
model.LocaleEnglish,
))
require.NoError(t, err)
defer reader.Close()

blocks := testutil.CollectBlocks(t, reader.Read(ctx))
require.GreaterOrEqual(t, len(blocks), 1)

// Plain text is the source runs with inline markup stripped.
assert.Equal(t, "Click here for info", blocks[0].SourceText())

// Inline codes are preserved as a PcOpen/PcClose pair on the source runs.
// (There is no Segment type; segmentation is an opt-in overlay, F-02.)
runs := blocks[0].SourceRuns()
require.Len(t, runs, 4) // "Click ", <b>, "here", </b> + trailing text coalesces
require.NotNil(t, runs[1].PcOpen)
assert.Equal(t, "<b>", runs[1].PcOpen.Data)
require.NotNil(t, runs[3].PcClose)
assert.Equal(t, "</b>", runs[3].PcClose.Data)
assert.Equal(t, runs[1].PcOpen.ID, runs[3].PcClose.ID) // shared ID
}

Test each type of inline element your format supports:

func TestReadPlaceholderRun(t *testing.T) {
// Self-closing elements become Ph runs
reader := NewReader()
reader.Open(ctx, testutil.RawDocFromString(
`<html><body><p>Line one<br/>Line two</p></body></html>`,
model.LocaleEnglish,
))
defer reader.Close()

blocks := testutil.CollectBlocks(t, reader.Read(ctx))
runs := blocks[0].SourceRuns()

assert.Equal(t, "Line oneLine two", blocks[0].SourceText())
// The <br/> is a single Ph run between the two text runs.
require.NotNil(t, runs[1].Ph)
assert.Equal(t, "br", runs[1].Ph.Type)
}

func TestReadLinkRun(t *testing.T) {
// Links preserve href in PcOpen.Data
reader := NewReader()
reader.Open(ctx, testutil.RawDocFromString(
`<html><body><p>Visit <a href="http://example.com">our site</a></p></body></html>`,
model.LocaleEnglish,
))
defer reader.Close()

blocks := testutil.CollectBlocks(t, reader.Read(ctx))
runs := blocks[0].SourceRuns()

assert.Equal(t, "Visit our site", blocks[0].SourceText())
require.NotNil(t, runs[1].PcOpen)
assert.Contains(t, runs[1].PcOpen.Data, "href") // Attributes preserved
}

Roundtrip Tests​

Read a file, write it back, and compare the result with the original. This proves that inline codes survive the full read-write cycle:

func TestRoundTrip(t *testing.T) {
original, err := os.ReadFile("testdata/sample.myf")
require.NoError(t, err)

ctx := context.Background()
reader := NewReader()
err = reader.Open(ctx, testutil.RawDocFromReader(
bytes.NewReader(original), "testdata/sample.myf", model.LocaleEnglish))
require.NoError(t, err)
parts := testutil.CollectParts(t, reader.Read(ctx))
reader.Close()

var buf bytes.Buffer
writer := NewWriter()
writer.SetOutputWriter(&buf)
writer.Write(ctx, testutil.PartsToChannel(parts))
writer.Close()

assert.Equal(t, string(original), buf.String())
}

Translation Roundtrip Tests​

Verify that translated content with inline codes writes correctly:

func TestTranslationRoundTrip(t *testing.T) {
ctx := context.Background()
reader := NewReader()
reader.Open(ctx, testutil.RawDocFromString(
`<html><body><p>Click <b>here</b></p></body></html>`,
model.LocaleEnglish,
))
parts := testutil.CollectParts(t, reader.Read(ctx))
reader.Close()

// Build a translated run sequence with the same inline codes.
for _, p := range parts {
if p.Type == model.PartBlock {
block := p.Resource.(*model.Block)

block.SetTargetRuns(model.LocaleFrench, []model.Run{
{Text: &model.TextRun{Text: "Cliquez "}},
{PcOpen: &model.PcOpenRun{ID: "1", Type: "fmt:bold", Data: "<b>"}},
{Text: &model.TextRun{Text: "ici"}},
{PcClose: &model.PcCloseRun{ID: "1", Type: "fmt:bold", Data: "</b>"}},
})
}
}

var buf bytes.Buffer
writer := NewWriter()
writer.SetOutputWriter(&buf)
writer.SetLocale(model.LocaleFrench)
writer.Write(ctx, testutil.PartsToChannel(parts))

assert.Contains(t, buf.String(), "Cliquez <b>ici</b>")
}

See Testing Strategy for more patterns.


Inline Code Patterns by Format​

Different formats map to the same Run model in different ways:

HTML / XML​

Block-level elements (<p>, <div>, <h1>) are Block boundaries. Inline elements (<b>, <a>, <em>, <span>) become PcOpen/PcClose pairs. Void elements (<br>, <img>) become Ph runs.

Input: <p>Click <b>here</b> for <a href="/help">info</a></p>
Text: "Click here for info"
Runs: [text, PcOpen <b>, text, PcClose </b>, text, PcOpen <a href="/help">, text, PcClose </a>]

Markdown​

Emphasis markers (*, **, `) become PcOpen/PcClose pairs. Links have the URL stored in the opening run's Data field.

Input: Click **here** for [info](/help)
Text: "Click here for info"
Runs: [text, PcOpen **, text, PcClose **, text, PcOpen [, text, PcClose ](/help)]

XLIFF / Translation Formats​

XLIFF <pc> maps to a PcOpen/PcClose pair, <bpt>/<ept> (begin/end paired tag) likewise. <ph> (placeholder) and <it> (isolated tag) map to a Ph run. The original XLIFF inline markup goes into the run's Data.

Templating / Variables​

Template variables like \{name\} or $\{count\} become Ph runs. The full variable expression goes into Data:

Input: Hello {name}, you have {count} items
Text: "Hello , you have items"
Runs: [text, Ph {name}, text, Ph {count}, text]

Formats Without Inline Codes​

Formats like JSON, YAML, or .properties typically don't have inline markup. Use model.NewBlock(id, text) to create a block with a single plain TextRun. If these formats contain embedded HTML or Markdown, use nested Layers (see Architecture) to delegate inline handling to the appropriate sub-format reader.