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 thePcOpen/PcClosepairs above- the second run's
PcOpen.Datais"<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:
| Category | Run kind | Examples | Pattern |
|---|---|---|---|
| Paired tags | PcOpen + PcClose | <b>...</b>, **...**, <a>...</a> | Wrap content with two runs (shared ID) |
| Self-closing | Ph | <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:
- 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.
- A declared encoding other than UTF-8 (
--encoding, the recipe'sdefaults.encoding) selects its decoder from thecore/encodingregistry. - 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: trueonBaseFormatWriter; its reader declaresBinary: trueon theFormatSignature. 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.encodingin 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:
| Field | Purpose | Example |
|---|---|---|
| discriminator | Which field is set: PcOpen, PcClose, or Ph | a PcOpen |
Type | Semantic type for tool processing | "fmt:bold", "fmt:link", "var" |
ID | Matches an opening run to its closing run | "1" shared by the <b>/</b> pair |
Data | Original markup for roundtrip reconstruction | "<a href=\"/help\">" |
Disp | UI label in translation editors | "[B]", "[/B]", "[IMG]" |
Equiv | Plain text equivalent | "\n" for <br> |
Constraints | Editing 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.
Related
- E-02: The format system: the reader and writer contracts and what each format declares.
- Interface Reference:
DataFormatReader,DataFormatWriterand the run types. - Testing Strategy: roundtrip and spec-suite tests.
- Plugin System: shipping a format out of tree.