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

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

Ţĥîš ĝüîđé éẋþļàîñš ĥöŵ ţö àđđ à ñéŵ đöçüḿéñţ ƒöŕḿàţ ţö neokapi, ƒŕöḿ ƃàšîç ŕéàđéŕš àñđ ŵŕîţéŕš ţö ƒüļļ îñļîñé çöđé šüþþöŕţ ŵîţĥ ŕöüñđţŕîþ ƒîđéļîţý.

Šţŕüçţüŕé

Çŕéàţé à þàçķàĝé üñđéŕ 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
}
}
}
}

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

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

neokapi šöļṽéš ţĥîš ŵîţĥ ţĥé Ŕüñ ḿöđéļ: à ƃļöçķ'š çöñţéñţ îš à ƒļàţ []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"> ţàĝ ŕöüñđţŕîþš àš éẋàçţļý ţĥàţ šţŕîñĝ, àţţŕîƃüţéš àñđ àļļ.

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

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

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

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


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

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

ƑîéļđÞüŕþöšéÉẋàḿþļé
đîšçŕîḿîñàţöŕŴĥîçĥ ƒîéļđ îš šéţ: 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 {
Encoding string `yaml:"encoding" json:"encoding"`
}

func (c *Config) FormatName() string { return "myformat" }
func (c *Config) Reset() { c.Encoding = "UTF-8" }
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. ΃ ţĥéšé ƒöŕḿàţš çöñţàîñ éḿƃéđđéđ ĤŢḾĻ öŕ Ḿàŕķđöŵñ, üšé ñéšţéđ Ļàýéŕš (šéé Àŕçĥîţéçţüŕé) ţö đéļéĝàţé îñļîñé ĥàñđļîñĝ ţö ţĥé àþþŕöþŕîàţé šüƃ-ƒöŕḿàţ ŕéàđéŕ.