Šķîþ ţö ḿàîñ çöñţéñţ

Îḿþļéḿéñţîñĝ à Ñéŵ Ƒöŕḿàţ

À ƒöŕḿàţ îš à ŕéàđéŕ ţĥàţ ţüŕñš à ƒîļé îñţö Ƃļöçķš àñđ à ŵŕîţéŕ ţĥàţ ŵŕîţéš ţĥéḿ ƃàçķ. Ýöü îḿþļéḿéñţ ƃöţĥ, çàŕŕý îñļîñé ḿàŕķüþ àš ŕüñš, àñđ þŕöṽé ţĥé ŕöüñđţŕîþ ŵîţĥ ţéšţš ƃéƒöŕé ýöü ŕéĝîšţéŕ ţĥé ƒöŕḿàţ.

Šţŕüçţüŕé​

Çŕéàţé à þàçķàĝé üñđéŕ core/formats/ ŵîţĥ ţĥéšé ƒîļéš:

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

Ŕéàđéŕ​

Ţĥé ŕéàđéŕ ḿüšţ îḿþļéḿéñţ format.DataFormatReader. Éḿƃéđ format.BaseFormatReader ƒöŕ šĥàŕéđ ƃéĥàṽîöŕ:

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
}

Ţĥé éẋàḿþļé àƃöṽé éḿîţš þļàîñ ţéẋţ. Ḿöšţ ŕéàļ-ŵöŕļđ ƒöŕḿàţš çöñţàîñ îñļîñé ḿàŕķüþ (ƃöļđ, ļîñķš, îḿàĝéš) ţĥàţ ḿüšţ ƃé þŕéšéŕṽéđ ţĥŕöüĝĥ ţĥé þîþéļîñé; šéé Îñļîñé Çöđé Ĥàñđļîñĝ ƃéļöŵ.

Ŵŕîţéŕ​

Ţĥé ŵŕîţéŕ ḿüšţ îḿþļéḿéñţ format.DataFormatWriter. Éḿƃéđ 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
}
}
}
}

Àđđîñĝ àñđ Ŕéḿöṽîñĝ Ƃļöçķš​

À ŵŕîţéŕ ţĥàţ çàñ àđđ à ƃļöçķ ţö à đöçüḿéñţ, öŕ ŕéḿöṽé öñé ŵîţĥ îţš ḿàŕķüþ, îḿþļéḿéñţš format.StructureEditor: Structural() ļîšţš insert_block àñđ delete_block, àñđ EditStructure(doc, edits) ŕéţüŕñš ţĥé đöçüḿéñţ ŵîţĥ éàçĥ éđîţ ḿàđé àñđ éṽéŕý öţĥéŕ ƃýţé àš îţ ŵàš. Îţ šüîţš à ƒöŕḿàţ ŵĥöšé ƃļöçķ šĥéļļ îš šḿàļļ, šüçĥ àš à çàţàļöĝ'š ķéý àñđ ṽàļüé. Structural() àñšŵéŕš ƒöŕ ţĥé ŵŕîţéŕ'š çöñƒîĝüŕàţîöñ: üñđéŕ öñé ţĥàţ ḿàķéš öţĥéŕ ḿàŕķüþ þàŕţ öƒ à ƃļöçķ'š šĥéļļ, šüçĥ àš à ñöţé ŕéàđ ƒŕöḿ ţĥé ḿéḿƃéŕ ƃéšîđé îţ, îţ ļîšţš ñöţĥîñĝ. À ñéŵ ƃļöçķ ĝöéš ŵĥéŕé ţĥé ŕéàđéŕ ŵöüļđ ļööķ ƒöŕ îţš ķéý, àñđ à çöḿḿéñţ ţĥé ŕéàđéŕ ŕéàđš àš à ƃļöçķ'š ñöţé ĝöéš ŵîţĥ ţĥé ƃļöçķ. Éàçĥ đéçļàŕàţîöñ ñééđš þàššîñĝ çéļļš îñ ţĥé šţŕüçţüŕàļ öþéŕàţîöñš ḿàţŕîẋ (core/formats/opsmatrix_structural_test.go), àñđ ţĥé ƒöŕḿàţ šýšţéḿ đéšçŕîƃéš ŵĥàţ ţĥé ĴŠÖÑ, ÝÀḾĻ àñđ ÀŔƂ ŵŕîţéŕš ŵŕîţé.


Îñļîñé Çöđé Ĥàñđļîñĝ​

Ḿöšţ đöçüḿéñţ ƒöŕḿàţš çöñţàîñ îñļîñé ḿàŕķüþ: ƃöļđ, îţàļîç, ļîñķš, îḿàĝéš, ļîñé ƃŕéàķš, ṽàŕîàƃļéš, þļàçéĥöļđéŕš. Ţĥé ƒŕàḿéŵöŕķ ḿüšţ þŕéšéŕṽé ţĥîš ḿàŕķüþ ţĥŕöüĝĥ ţĥé éñţîŕé þîþéļîñé (éẋţŕàçţîöñ, çöñţéñţ-ḿéḿöŕý ļööķüþ, ḾŢ, ÀÎ ţŕàñšļàţîöñ, çĥéçķš, ŕéçöñšţŕüçţîöñ) ŵîţĥöüţ çöŕŕüþţîöñ.

ñéöķàþî šöļṽéš ţĥîš ŵîţĥ ţĥé Ŕüñ ḿöđéļ: à ƃļöçķ'š çöñţéñţ îš à ƒļàţ []model.Run šéǫüéñçé. Ţéẋţ ţŕàṽéļš àš TextRunš; îñļîñé ḿàŕķüþ ƃéçöḿéš îñļîñé-çöđé ŕüñš (PcOpen/PcClose ƒöŕ þàîŕéđ ţàĝš, Ph ƒöŕ šéļƒ-çļöšîñĝ ţöķéñš) ţĥàţ çàŕŕý ţĥé öŕîĝîñàļ ḿàŕķüþ îñ à Data ƒîéļđ. Ţĥîš ļéţš ţööļš, ţŕàñšļàţîöñ éñĝîñéš, àñđ çöñţéñţ-ḿéḿöŕý ḿàţçĥîñĝ þŕöĵéçţ ţĥé ŕüñš ţö þļàîñ ţéẋţ, àñđ ţĥé ŵŕîţéŕ ŕéçöñšţŕüçţš ţĥé öŕîĝîñàļ ḿàŕķüþ ƃý ŕé-éḿîţţîñĝ éàçĥ ŕüñ'š Data.

Ţĥé Ŕüñ Ḿöđéļ​

À Run îš à đîšçŕîḿîñàţéđ üñîöñ: éẋàçţļý öñé öƒ îţš þöîñţéŕ ƒîéļđš îš šéţ:

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
}

Ţĥé ţĥŕéé îñļîñé-çöđé ŕüñš ýöü ŕéàçĥ ƒöŕ ḿöšţ àŕé:

// 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 îš ţĥé éđîţîñĝ-þöļîçý ţŕîþļé:

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
}

Ĥöŵ Îţ Ŵöŕķš​

Çöñšîđéŕ ţĥîš ĤŢḾĻ þàŕàĝŕàþĥ:

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

Ţĥé ŕéàđéŕ éẋţŕàçţš ţĥé <p> çöñţéñţ àš à šîñĝļé ƃļöçķ ŵĥöšé Runs àŕé:

[
{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>"}},
]

Ţĥé ŕüñš àŕé öŕđéŕéđ, àñđ à PcClose šĥàŕéš îţš ID ŵîţĥ ţĥé ḿàţçĥîñĝ PcOpen. Ţĥîš ḿéàñš:

  • block.SourceText() ŕéţüŕñš "Click here for info" (îñļîñé-çöđé ŕüñš çöñţŕîƃüţé ñöţĥîñĝ)
  • block.SourceRuns() çöñţàîñš ţĥé PcOpen/PcClose þàîŕš àƃöṽé
  • ţĥé šéçöñđ ŕüñ'š PcOpen.Data îš "<b>" (ţĥé öŕîĝîñàļ ḿàŕķüþ, îñçļüđîñĝ àţţŕîƃüţéš)

Ţööļš þŕöĵéçţ ţĥé ŕüñš ţö þļàîñ ţéẋţ àñđ šķîþ ţĥé îñļîñé çöđéš. Ţŕàñšļàţîöñ éñĝîñéš ĝéţ çļéàñ ţéẋţ ŵîţĥ öþàǫüé ţöķéñš. Ţĥé ŵŕîţéŕ ŕé-éḿîţš éàçĥ ŕüñ'š Data ţö ŕéçöñšţŕüçţ ţĥé öŕîĝîñàļ ḿàŕķüþ, àţţŕîƃüţéš šüçĥ àš class="emphasis" öŕ href="/help" îñçļüđéđ.

Ţĥŕéé Çàţéĝöŕîéš öƒ Îñļîñé Éļéḿéñţš​

Ŵĥéñ îḿþļéḿéñţîñĝ à ƒöŕḿàţ ŕéàđéŕ, çļàššîƒý éàçĥ îñļîñé éļéḿéñţ îñţö öñé öƒ ţĥŕéé çàţéĝöŕîéš:

ÇàţéĝöŕýŔüñ ķîñđÉẋàḿþļéšÞàţţéŕñ
Þàîŕéđ ţàĝšPcOpen + PcClose<b>...</b>, **...**, <a>...</a>Ŵŕàþ çöñţéñţ ŵîţĥ ţŵö ŕüñš (šĥàŕéđ ÎĐ)
Šéļƒ-çļöšîñĝPh<br/>, <img>, <hr/>Šîñĝļé ŕüñ, ñö çĥîļđŕéñ
Ƃļöçķ-ļéṽéļ(ñöţ à ŕüñ)<p>, <div>, <h1>Ƃöüñđàŕý ƒöŕ à ñéŵ Ƃļöçķ

Ţĥé ŕéàđéŕ đéçîđéš ŵĥàţ îš îñļîñé ṽš. ƃļöçķ-ļéṽéļ. Ƒöŕ ĤŢḾĻ, ţĥîš đîšţîñçţîöñ îš ŵéļļ-đéƒîñéđ. Ƒöŕ öţĥéŕ ƒöŕḿàţš (Ḿàŕķđöŵñ, ẊĻÎƑƑ, çüšţöḿ ẊḾĻ), ýöü çĥööšé ţĥé ḿàþþîñĝ ƃàšéđ öñ ŵĥàţ à ţŕàñšļàţöŕ ñééđš ţö šéé àš à çöñţîĝüöüš üñîţ.

Çöḿþļéţé Ŕéàđéŕ Éẋàḿþļé ŵîţĥ Îñļîñé Çöđéš​

Ĥéŕé îš ĥöŵ à ŕéàđéŕ çöļļéçţš îñļîñé çöñţéñţ ƒŕöḿ à ƃļöçķ-ļéṽéļ éļéḿéñţ îñţö à []model.Run šļîçé. Ţĥîš þàţţéŕñ àþþļîéš ţö àñý ƒöŕḿàţ ŵîţĥ îñļîñé ḿàŕķüþ:

// 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
}
}
}

Ŕéçüŕšé îñţö îñļîñé çĥîļđŕéñ ţö ĥàñđļé ñéšţéđ ƒöŕḿàţţîñĝ ļîķé <b><i>bold italic</i></b>. Éàçĥ ļéṽéļ öƒ ñéšţîñĝ àþþéñđš îţš öŵñ PcOpen/PcClose þàîŕ, àñđ ţĥé ƒļàţ ŕüñ šéǫüéñçé ķééþš ţĥéḿ îñ öŕđéŕ. Àţţàçĥ ţĥé çöļļéçţéđ ŕüñš ţö à ƃļöçķ ŵîţĥ block.SetSourceRuns(runs), öŕ ƃüîļđ ţĥé ƃļöçķ đîŕéçţļý ŵîţĥ model.NewRunsBlock(id, runs).

Ŕéçöñšţŕüçţîñĝ Ḿàŕķüþ îñ à Ŵŕîţéŕ​

Ţĥé ŵŕîţéŕ ŵàļķš ţĥé ŕüñ šéǫüéñçé àñđ éḿîţš éàçĥ ŕüñ'š çöñţéñţ: ţĥé ţéẋţ öƒ à TextRun, àñđ ţĥé çàþţüŕéđ Data öƒ àñ îñļîñé-çöđé ŕüñ. Ţĥé ƒŕàḿéŵöŕķ þŕöṽîđéš model.RenderRunsWithData, ŵĥîçĥ éḿîţš ƃöţĥ ṽéŕƃàţîḿ:

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))
}

Ŕéþļàýîñĝ Data ķééþš îñļîñé ḿàŕķüþ ƒàîţĥƒüļ: àñ <a href="/help" class="nav"> ţàĝ ŕöüñđţŕîþš àš éẋàçţļý ţĥàţ šţŕîñĝ, àţţŕîƃüţéš àñđ àļļ, ŵîţĥöüţ ţĥé ŵŕîţéŕ üñđéŕšţàñđîñĝ îţ.

À TextRun ŵŕîţţéñ ƃý à ţŕàñšļàţîöñ, à þšéüđö-ţŕàñšļàţîöñ, öŕ àñ éđîţ ḿàđé ţĥŕöüĝĥ kapi apply, ksed öŕ ḾÇÞ apply_edits ĥöļđš ţéẋţ: îñļîñé çöđéš ţŕàṽéļ àš ţĥéîŕ öŵñ ŕüñš, šö à < öŕ & îñ öñé îš à çĥàŕàçţéŕ. À ŵŕîţéŕ ƒöŕ à ƒöŕḿàţ ŵîţĥ ḿàŕķüþ öŕ éšçàþéš éñçöđéš ţĥöšé ŕüñš. À ƃļöçķ àñ éđîţ ŕéŵŕöţé šàýš šö: ţööļš ŕéþļàçé à šöüŕçé ţĥŕöüĝĥ model.Block.EditSourceRuns, àñđ Block.SourceAsRead ŕéţüŕñš ţĥé šöüŕçé ţĥé ŕéàđéŕ þŕöđüçéđ àñđ ŵĥéţĥéŕ ţĥé çüŕŕéñţ öñé đéŕš. Ŵŕîţé à ƃļöçķ ñö éđîţ çĥàñĝéđ àš îţ ŵàš ŕéàđ, šö ţĥé đöçüḿéñţ ķééþš îţš ƃýţéš, àñđ éñçöđé àñ éđîţéđ öñé. model.RenderRunsWith îš ţĥé ŕüñ ŵàļķ RenderRunsWithData üšéš, ŵîţĥ à ĥööķ ţĥàţ ŵŕîţéš éàçĥ ţéẋţ ŕüñ îñ ýöüŕ ƒöŕḿàţ'š šþéļļîñĝ; ţĥé ĤŢḾĻ ŵŕîţéŕ üšéš îţ ţö ŵŕîţé éṽéŕý < àñđ & öƒ àñ éđîţéđ ƃļöçķ àš à ŕéƒéŕéñçé.

Šöḿé ŕéàđéŕš ļéàṽé šýñţàẋ ţĥéý đö ñöţ ḿöđéļ îñ ţéẋţ ŕüñš: ţĥé Ḿàŕķđöŵñ ŕéàđéŕ ķééþš ƃàçķšļàšĥ éšçàþéš àñđ ƃŕàçķéţš ţĥàţ öþéñ ñö ļîñķ, ţĥé ḾĐẊ ŕéàđéŕ ķééþš éẋþŕéššîöñš, ţĥé ÀšçîîĐöç ŕéàđéŕ ķééþš þàššţĥŕöüĝĥš àñđ ḿàçŕöš. Šüçĥ à ŕüñ îš ţĥé đöçüḿéñţ'š öŵñ ḿàŕķüþ üñţîļ àñ éđîţ ŕéŵŕîţéš îţ, àñđ ţĥéñ ţĥé ŵŕîţéŕ çàññöţ ţéļļ ḿàŕķüþ ƒŕöḿ ŵöŕđîñĝ ƃý ţĥé çĥàŕàçţéŕš àļöñé. format.RenderEditedRuns çöḿþàŕéš ţĥé éđîţéđ ŕüñš ŵîţĥ ţĥé ŕüñš àš ŕéàđ: ḿàŕķüþ àñ AddedSyntax ƒîñđš ţĥàţ ţĥé ƃļöçķ àļŕéàđý ĥéļđ, šþéļļéđ ţĥé šàḿé, šţàýš, àñđ ḿàŕķüþ ţĥé éđîţ àđđš îš éšçàþéđ.

Ţĥŕéé šŵééþš îñ core/formats/escape_symmetry_test.go ĥöļđ ţĥé ƒöŕḿàţš ţö ţĥîš. TestEscapeSymmetryOnModify ŵŕîţéš à çöŕþüš öƒ çĥàŕàçţéŕš îñţö à ţŕàñšļàţéđ ṽàļüé, TestEscapeSymmetryOnSourceEdit ŵŕîţéš îţ îñţö àñ éđîţéđ šöüŕçé, àñđ TestEditedWordingReadsBackAsText éđîţš éàçĥ ḿàŕķüþ ƒöŕḿàţ ŵîţĥ ŵöŕđîñĝ ţĥàţ šþéļļš ţàĝš, ļîñķš, éẋþŕéššîöñš àñđ ḿàçŕöš, àñđ ŕéàđš ţĥé ŕéšüļţ ƃàçķ àš ţéẋţ. Ţĥé ƒîŕšţ ţŵö çöṽéŕ éṽéŕý ƒöŕḿàţ ŵĥöšé ŕéàđéŕ àñđ ŵŕîţéŕ šĥàŕé à šķéļéţöñ, ţĥé ŽÎÞ çöñţàîñéŕš îñçļüđéđ; TestEscapeSweepCoversEverySkeletonPair ƒàîļš ŵĥéñ à ƒöŕḿàţ ĝàîñš à šķéļéţöñ þàîŕ öüţšîđé ţĥéḿ.

Àţţŕîƃüţéš àñđ Ñéŵ Çöđéš à Ŵŕîţéŕ Çàñ Ŵŕîţé​

Ŕéþļàýîñĝ Data ķééþš à çöđé àš îţ ŵàš ŕéàđ. À ŵŕîţéŕ ţĥàţ çàñ àļšö ŵŕîţé à çĥàñĝéđ àţţŕîƃüţé öƒ à çöđé, öŕ à ñéŵ çöđé àŕöüñđ ţéẋţ, đéçļàŕéš îţ, àñđ ţĥé çĥàñĝé çöñţŕàçţ'š set_attribute, mark àñđ ñéŵ çöđéš îñ à runs þàýļöàđ ŕéàçĥ ţĥé ƒöŕḿàţ öñļý ŵĥéŕé îţ đöéš:

// 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)

Ţĥé çĥàñĝé àþþļîéŕ çàļļš ţĥéšé ŵĥéñ îţ àþþļîéš ţĥé öþéŕàţîöñ, šö ţĥé ƃļöçķ àļŕéàđý ĥöļđš ţĥé ƃýţéš ţĥé ŵŕîţéŕ ŵŕîţéš. Ŕéƒüšé ŵĥàţ ţĥé ƒöŕḿàţ çàññöţ çàŕŕý ŵîţĥ à ŕéàšöñ à çàļļéŕ çàñ àçţ öñ: ţĥé Ḿàŕķđöŵñ ŵŕîţéŕ ŕéƒüšéš à ŕéƒéŕéñçé ļîñķ'š đéšţîñàţîöñ ƃéçàüšé ţĥé đéƒîñîţîöñ éļšéŵĥéŕé ḿàý šéŕṽé öţĥéŕ ļîñķš. Éàçĥ đéçļàŕàţîöñ ñééđš à çéļļ îñ ţĥé öþéŕàţîöñš ḿàţŕîẋ (core/formats/opsmatrix_capabilities_test.go) ţĥàţ ŵŕîţéš îţ àñđ ŕéàđš îţ ƃàçķ; ţĥé ƃüîļđ ƒàîļš öñ à đéçļàŕàţîöñ ŵîţĥöüţ öñé. É-02 ļîšţš ŵĥàţ éàçĥ ƒöŕḿàţ đéçļàŕéš.

Çĥööšîñĝ Ţàŕĝéţ ṽš Šöüŕçé Çöñţéñţ​

Ŵĥéñ ŵŕîţîñĝ öüţþüţ, ţĥé ŵŕîţéŕ ḿüšţ çĥööšé ţĥé ŕîĝĥţ çöñţéñţ. Üšé ţĥé ţàŕĝéţ ŕüñš îƒ à ţŕàñšļàţîöñ éẋîšţš ƒöŕ ţĥé çöñƒîĝüŕéđ ļöçàļé, öţĥéŕŵîšé ƒàļļ ƃàçķ ţö ţĥé šöüŕçé ŕüñš:

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())
}
}

Üšîñĝ Šķéļéţöñš ƒöŕ Đöçüḿéñţ Šţŕüçţüŕé​

Ƃļöçķ-ļéṽéļ šţŕüçţüŕé ţĥàţ šüŕŕöüñđš ţŕàñšļàţàƃļé çöñţéñţ (öþéñîñĝ àñđ çļöšîñĝ ţàĝš, ŵĥîţéšþàçé, éţç.) îš çàþţüŕéđ îñ à Šķéļéţöñ. Ţĥé ŕéàđéŕ ƃüîļđš à šķéļéţöñ ŵîţĥ ţéẋţ þàŕţš àñđ à ŕéƒéŕéñçé ţö ţĥé ƃļöçķ çöñţéñţ:

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
},
}

Ţĥé ŵŕîţéŕ üšéš ţĥé šķéļéţöñ ţö ŕéçöñšţŕüçţ ţĥé đöçüḿéñţ:

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)
}
}
}

Ţĥé šķéļéţöñ ķééþš ţĥé ƃļöçķ-ļéṽéļ šţŕüçţüŕé öƒ ţĥé đöçüḿéñţ îñţàçţ ţĥŕöüĝĥ à ŕöüñđţŕîþ. À ŵŕîţéŕ ŵîţĥöüţ öñé ĥàš ţö ĝéñéŕàţé ţĥé šüŕŕöüñđîñĝ ţàĝš, ŵĥîţéšþàçé àñđ àţţŕîƃüţéš àĝàîñ, àñđ çàñ ļöšé îñƒöŕḿàţîöñ đöîñĝ šö.


Đöçüḿéñţ éñçöđîñĝ​

À ŕéàđéŕ ñéṽéŕ đéçöđéš à çĥàŕšéţ. Éṽéŕý çàļļéŕ öþéñš à đöçüḿéñţ ţĥŕöüĝĥ format.OpenDocument, ţĥé îñĝéšţîöñ šéàḿ, ŵĥîçĥ šéţţļéš ţĥé đöçüḿéñţ'š éñçöđîñĝ àĝàîñšţ îţš ƃýţéš àñđ ĥàñđš ţĥé ŕéàđéŕ ÜŢƑ-8:

  1. À ƃýţé-öŕđéŕ ḿàŕķ ŵîñš. Îţ îš éṽîđéñçé îñ ţĥé ƒîļé: à ÜŢƑ-8 ḿàŕķ ķééþš ţĥé ƃýţéš àš ţĥéý àŕé, à ÜŢƑ-16 ḿàŕķ šéļéçţš ţĥàţ đéçöđéŕ, ŵĥàţéṽéŕ ŵàš đéçļàŕéđ.
  2. À đéçļàŕéđ éñçöđîñĝ öţĥéŕ ţĥàñ ÜŢƑ-8 (--encoding, ţĥé ŕéçîþé'š defaults.encoding) šéļéçţš îţš đéçöđéŕ ƒŕöḿ ţĥé core/encoding ŕéĝîšţŕý.
  3. Ŵîţĥ ñéîţĥéŕ, ţĥé ƃýţéš þàšš ţĥŕöüĝĥ àš ÜŢƑ-8.

Öñ ŕéţüŕñ doc.Encoding ñàḿéš ţĥé éñçöđîñĝ ţĥé đöçüḿéñţ ŵàš ŕéàđ àš, àñđ ţĥé ŕéàđéŕ çöþîéš îţ öñţö ţĥé ŕööţ Layer. doc.EncodingSettled ŕéþöŕţš ŵĥéţĥéŕ à ḿàŕķ öŕ à đéçļàŕàţîöñ šéţţļéđ îţ. À ŕéàđéŕ ŵîţĥ à çĥàŕšéţ ĥéüŕîšţîç öƒ îţš öŵñ, šüçĥ àš ţĥé ẊḾĻ þŕöļöĝ öŕ ţĥé ÞÖ ĥéàđéŕ, àþþļîéš îţ öñļý ŵĥéñ ţĥé đöçüḿéñţ îš üñšéţţļéđ; à šéţţļéđ đöçüḿéñţ îš ÜŢƑ-8 ŵĥàţéṽéŕ îţš þŕöļöĝ šàýš, àñđ format.XMLCharsetReader(r.Doc) îš ţĥé encoding/xml CharsetReader ţĥàţ ķééþš ţĥé đéçöđéŕ ƒŕöḿ ŕéƒüšîñĝ ţĥé þŕöļöĝ. Ķééþ ţĥé þŕöļöĝ îñ ţĥé šķéļéţöñ àš îţ îš: ţĥé ŵŕîţéŕ éñçöđéš îţ ƃàçķ îñ ţĥé šàḿé çĥàŕšéţ.

Ŵŕîţé-ƃàçķ ḿîŕŕöŕš ţĥé ŕéàđ. SetEncoding öñ BaseFormatWriter îñšţàļļš ţĥé éñçöđéŕ ƒöŕ ţĥé ŕüñ'š çĥàŕšéţ, šö à ŵŕîţéŕ šéŕîàļîšéš ÜŢƑ-8 àñđ ţĥé ƃàšé éñçöđéš ţĥé ŵĥöļé öüţþüţ šţŕéàḿ. Ţĥé ŕüļé ƃý ŵŕîţéŕ ƒàḿîļý:

  • À šķéļéţöñ-ƃàšéđ ŵŕîţéŕ àñđ à ĝéñéŕàţîṽé ŵŕîţéŕ ƃöţĥ éḿîţ ţéẋţ, šö ţĥé ƃàšé ŕé-éñçöđéš éṽéŕýţĥîñĝ ţĥéý ŵŕîţé. À šķéļéţöñ ĥöļđš ţĥé šöüŕçé'š đéçöđéđ ţéẋţ, šö àñ üñţöüçĥéđ đöçüḿéñţ îš ŵŕîţţéñ ƃàçķ ƃýţé ƒöŕ ƃýţé, þŕöļöĝ îñçļüđéđ.
  • À ƃýţé-ƒàîţĥƒüļ ŵŕîţéŕ (à ŽÎÞ çöñţàîñéŕ, àñ îḿàĝé, à çöḿþîļéđ çàţàļöĝ, öŕ à þļüĝîñ ŵĥöšé þŕöçéšš éñçöđéš îţš öŵñ öüţþüţ) đéçļàŕéš Binary: true öñ BaseFormatWriter; îţš ŕéàđéŕ đéçļàŕéš Binary: true öñ ţĥé FormatSignature. Ţĥé šéàḿ ĥàñđš šüçĥ à ŕéàđéŕ ţĥé ƃýţéš àš ţĥéý àŕé, àñđ ţĥé ƃàšé àþþļîéš ñö çĥàŕšéţ àñđ ñö öüţþüţ öþţîöñ ţö šüçĥ à ŵŕîţéŕ.
  • À ƃýţé-öŕđéŕ ḿàŕķ ţĥé çĥàŕšéţ çàññöţ çàŕŕý ķééþš ţĥé öüţþüţ ÜŢƑ-8, ḿàŕķ îñçļüđéđ: ţĥé ḿàŕķ îš ŵĥàţ ţĥé ñéẋţ ŕéàđ šéţţļéš ţĥé éñçöđîñĝ ƃý.
  • output.encoding îñ à ƒöŕḿàţ'š çöñƒîĝ ŵîñš öṽéŕ ţĥé ŕüñ'š éñçöđîñĝ.

TestDeclaredEncodingSelectsTheCodec àñđ TestDeclaredEncodingRoundTrip îñ core/formats àŕé ţĥé çöñƒöŕḿàñçé ţéšţš: éṽéŕý ţéẋţ ƒöŕḿàţ ŕéàđš àñ ΊÖ-8859-1, ŵîñđöŵš-1252 àñđ Šĥîƒţ_ĴΊ đöçüḿéñţ đéçļàŕéđ àš šüçĥ àñđ ŵŕîţéš îţ ƃàçķ üñçĥàñĝéđ. À ñéŵ ƒöŕḿàţ ĵöîñš ţĥé šŵééþ ţĥŕöüĝĥ bomSweepFormats.


Ŕüñ Ḿéţàđàţà Ƒîéļđš​

Àñ îñļîñé-çöđé ŕüñ çàŕŕîéš ţĥé ŕàŵ ḿàŕķüþ àñđ šéṽéŕàļ ƒîéļđš ƃéšîđé îţ. Ţĥéšé ƒîéļđš ĥéļþ ţööļš, éđîţöŕš, àñđ çĥéçķš ŵöŕķ ŵîţĥ îñļîñé çöđéš îñţéļļîĝéñţļý:

ƑîéļđÞüŕþöšéÉẋàḿþļé
đîšçŕîḿîñàţöŕŴĥîçĥ ƒîéļđ îš šéţ: PcOpen, PcClose, öŕ Phà PcOpen
TypeŠéḿàñţîç ţýþé ƒöŕ ţööļ þŕöçéššîñĝ"fmt:bold", "fmt:link", "var"
IDḾàţçĥéš àñ öþéñîñĝ ŕüñ ţö îţš çļöšîñĝ ŕüñ"1" šĥàŕéđ ƃý ţĥé <b>/</b> þàîŕ
DataÖŕîĝîñàļ ḿàŕķüþ ƒöŕ ŕöüñđţŕîþ ŕéçöñšţŕüçţîöñ"<a href=\"/help\">"
DispÜÎ ļàƃéļ îñ ţŕàñšļàţîöñ éđîţöŕš"[B]", "[/B]", "[IMG]"
EquivÞļàîñ ţéẋţ éǫüîṽàļéñţ"\n" ƒöŕ <br>
ConstraintsÉđîţîñĝ þöļîçý (Deletable/Cloneable/Reorderable)ñöñ-đéļéţàƃļé ƒöŕ à {count} ṽàŕîàƃļé

Šéţ ţĥéšé ƒîéļđš îñ ţĥé ŕéàđéŕ ŵĥéñ ýöü ĥàṽé ţĥé îñƒöŕḿàţîöñ. Àţ ḿîñîḿüḿ, šéţ ţĥé đîšçŕîḿîñàţöŕ, Type, ID, àñđ Data. Ţĥé öţĥéŕ ƒîéļđš àŕé öþţîöñàļ; éđîţöŕš àñđ ţööļš üšé ţĥéḿ ŵĥéñ ţĥéý àŕé šéţ.


Çöñƒîĝüŕàţîöñ​

DataFormatConfig ŕéǫüîŕéš ƒöüŕ ḿéţĥöđš: FormatName(), Reset(), Validate(), àñđ ApplyMap(values map[string]any) error. ApplyMap àþþļîéš çöñƒîĝ ṽàļüéš ƒŕöḿ à ḿàþ àñđ ŕéĵéçţš üñķñöŵñ ķéýš àñđ ţýþé ḿîšḿàţçĥéš. Ţĥé format.ApplyMapViaJSON ĥéļþéŕ (core/format/applymap.go) îḿþļéḿéñţš ţĥîš ƒöŕ šţŕüçţ çöñƒîĝš ṽîà ĴŠÖÑ ḿàŕšĥàļ/üñḿàŕšĥàļ ŵîţĥ DisallowUnknownFields, ŵĥîļé çöñƒîĝš ŵîţĥ çöḿþļéẋ þàŕšîñĝ çàñ ĥàñđ-ŵŕîţé à šŵîţçĥ-ƃàšéđ ApplyMap (šéé core/formats/json/config.go). Ţĥé ĥéļþéŕ ŕéǫüîŕéš ţĥé šţŕüçţ'š ƒîéļđš ţö çàŕŕý ḿàţçĥîñĝ yaml/json ţàĝš ƒöŕ ţĥé îñçöḿîñĝ ķéýš.

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)
}

Ŕéĝîšţŕàţîöñ​

Àđđ ýöüŕ ƒöŕḿàţ îñšîđé formats.RegisterAll() îñ core/formats/register.go. RegisterReader ţàķéš ţĥé ƒöŕḿàţ ñàḿé, à ŕéàđéŕ ƒàçţöŕý, à FormatSignature ƒöŕ đéţéçţîöñ, àñđ à đîšþļàý ñàḿé; RegisterWriter ţàķéš ţĥé ñàḿé àñđ à ŵŕîţéŕ ƒàçţöŕý:

// 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()
})

Ţéšţîñĝ​

Ţĥé ĥéļþéŕš üšéđ ƃéļöŵ (testutil.RawDocFromString, RawDocFromReader, CollectBlocks, CollectParts, PartsToChannel) ļîṽé îñ github.com/neokapi/neokapi/core/internal/testutil. Ƃéîñĝ àñ îñţéŕñàļ þàçķàĝé, îţ îš îḿþöŕţàƃļé öñļý ƃý ƒöŕḿàţš îñšîđé ţĥîš ŕéþöšîţöŕý. À þļüĝîñ ƒöŕḿàţ îñ àñöţĥéŕ ŕéþöšîţöŕý ƃüîļđš ţĥé model.RawDocument àñđ đŕàîñš ţĥé Read çĥàññéļ îţšéļƒ; éàçĥ ĥéļþéŕ îš à ƒéŵ ļîñéš.

Éẋţŕàçţîöñ Ţéšţš​

Ṽéŕîƒý ţĥàţ ţĥé ŕéàđéŕ îđéñţîƒîéš ţĥé ţéẋţ àñđ ţĥé îñļîñé çöđéš:

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
}

Ţéšţ éàçĥ ţýþé öƒ îñļîñé éļéḿéñţ ýöüŕ ƒöŕḿàţ šüþþöŕţš:

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
}

Ŕöüñđţŕîþ Ţéšţš​

Ŕéàđ à ƒîļé, ŵŕîţé îţ ƃàçķ, àñđ çöḿþàŕé ţĥé ŕéšüļţ ŵîţĥ ţĥé öŕîĝîñàļ. Ţĥîš þŕöṽéš ţĥàţ îñļîñé çöđéš šüŕṽîṽé ţĥé ƒüļļ ŕéàđ-ŵŕîţé çýçļé:

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())
}

Ţŕàñšļàţîöñ Ŕöüñđţŕîþ Ţéšţš​

Ṽéŕîƒý ţĥàţ ţŕàñšļàţéđ çöñţéñţ ŵîţĥ îñļîñé çöđéš ŵŕîţéš çöŕŕéçţļý:

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>")
}

Šéé Ţéšţîñĝ Šţŕàţéĝý ƒöŕ ḿöŕé þàţţéŕñš.


Îñļîñé Çöđé Þàţţéŕñš ƃý Ƒöŕḿàţ​

Đéŕéñţ ƒöŕḿàţš ḿàþ ţö ţĥé šàḿé Ŕüñ ḿöđéļ îñ đéŕéñţ ŵàýš:

ĤŢḾĻ / ẊḾĻ​

Ƃļöçķ-ļéṽéļ éļéḿéñţš (<p>, <div>, <h1>) àŕé Ƃļöçķ ƃöüñđàŕîéš. Îñļîñé éļéḿéñţš (<b>, <a>, <em>, <span>) ƃéçöḿé PcOpen/PcClose þàîŕš. Ṽöîđ éļéḿéñţš (<br>, <img>) ƃéçöḿé Ph ŕüñš.

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>]

Ḿàŕķđöŵñ​

Éḿþĥàšîš ḿàŕķéŕš (*, **, `) ƃéçöḿé PcOpen/PcClose þàîŕš. Ļîñķš ĥàṽé ţĥé ÜŔĻ šţöŕéđ îñ ţĥé öþéñîñĝ ŕüñ'š Data ƒîéļđ.

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

ẊĻÎƑƑ / Ţŕàñšļàţîöñ Ƒöŕḿàţš​

ẊĻÎƑƑ <pc> ḿàþš ţö à PcOpen/PcClose þàîŕ, <bpt>/<ept> (ƃéĝîñ/éñđ þàîŕéđ ţàĝ) ļîķéŵîšé. <ph> (þļàçéĥöļđéŕ) àñđ <it> (îšöļàţéđ ţàĝ) ḿàþ ţö à Ph ŕüñ. Ţĥé öŕîĝîñàļ ẊĻÎƑƑ îñļîñé ḿàŕķüþ ĝöéš îñţö ţĥé ŕüñ'š Data.

Ţéḿþļàţîñĝ / Ṽàŕîàƃļéš​

Ţéḿþļàţé ṽàŕîàƃļéš ļîķé \{name\} öŕ $\{count\} ƃéçöḿé Ph ŕüñš. Ţĥé ƒüļļ ṽàŕîàƃļé éẋþŕéššîöñ ĝöéš îñţö Data:

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

Ƒöŕḿàţš Ŵîţĥöüţ Îñļîñé Çöđéš​

Ƒöŕḿàţš ļîķé ĴŠÖÑ, ÝÀḾĻ, öŕ .þŕöþéŕţîéš ţýþîçàļļý đöñ'ţ ĥàṽé îñļîñé ḿàŕķüþ. Üšé model.NewBlock(id, text) ţö çŕéàţé à ƃļöçķ ŵîţĥ à šîñĝļé þļàîñ TextRun. ΃ ţĥéšé ƒöŕḿàţš çöñţàîñ éḿƃéđđéđ ĤŢḾĻ öŕ Ḿàŕķđöŵñ, üšé ñéšţéđ Ļàýéŕš (šéé Àŕçĥîţéçţüŕé) ţö đéļéĝàţé îñļîñé ĥàñđļîñĝ ţö ţĥé àþþŕöþŕîàţé šüƃ-ƒöŕḿàţ ŕéàđéŕ.

Ŕéļàţéđ​