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

Àüţĥöŕîñĝ Ṽöçàƃüļàŕîéš

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

Ṽöçàƃüļàŕý ƒîļé ƒöŕḿàţ

Éàçĥ ṽöçàƃüļàŕý îš à ĴŠÖÑ ƒîļé. Ţýþéš àŕé ķéýéđ ƃý à category:name îđéñţîƒîéŕ àñđ çàŕŕý ŕéñđéŕîñĝ, đîšþļàý, çöļöŕ, àñđ çöñšţŕàîñţ ḿéţàđàţà:

{
"name": "my-vocabulary",
"version": "1.0",
"extends": "common-formatting",
"entity_prefix": "entity:",
"types": {
"category:type-name": {
"category": "category-name",
"label": "Human Readable Label",
"html": {
"open": "<tag>",
"close": "</tag>",
"placeholder": "<tag/>"
},
"display": {
"open": "[TAG]",
"close": "[/TAG]",
"placeholder": "[TAG/]"
},
"chipLabel": {
"open": "tag>",
"close": "/tag",
"placeholder": "tag"
},
"color": {
"bg": "rgba(59,130,246,0.15)",
"border": "rgba(59,130,246,0.5)",
"text": "rgb(59,130,246)"
},
"equiv": "",
"constraints": {
"deletable": true,
"cloneable": true,
"reorderable": true
}
}
},
"fallback": {
"html": { "open": "<span>", "close": "</span>", "placeholder": "<span/>" },
"display": { "open": "[?]", "close": "[/?]", "placeholder": "[?/]" },
"chipLabel": { "open": "?>", "close": "/?", "placeholder": "?" },
"color": {
"bg": "rgba(156,163,175,0.15)",
"border": "rgba(156,163,175,0.5)",
"text": "rgb(107,114,128)"
},
"constraints": { "deletable": true, "cloneable": true, "reorderable": true }
}
}

Ƒîéļđ ŕéƒéŕéñçé

ƑîéļđŔéǫüîŕéđĐéšçŕîþţîöñ
nameÝéšÜñîǫüé ṽöçàƃüļàŕý ñàḿé
versionÝ隊éḿṽéŕ ṽéŕšîöñ šţŕîñĝ
extendsÑöÞàŕéñţ ṽöçàƃüļàŕý ñàḿé (ţýþéš àŕé ḿéŕĝéđ)
entity_prefixÑöÞŕéƒîẋ ƒöŕ éñţîţý-ţýþé îñļîñé çöđéš (đéƒàüļţ "entity:")
typesÝéšḾàþ öƒ ţýþé ñàḿé → SpanTypeInfo
fallbackÑöĐéƒàüļţ ŕéñđéŕîñĝ ƒöŕ üñķñöŵñ ţýþéš

Ţýþé ñàḿé çöñṽéñţîöñ

Ţýþé ñàḿéš ƒöļļöŵ ţĥé category:name þàţţéŕñ: fmt:bold, link:hyperlink, code:variable, struct:break.

Çöñšţŕàîñţ šéḿàñţîçš

Çöñšţŕàîñţtruefalse
deletableŢŕàñšļàţöŕ ḿàý ŕéḿöṽé ţĥé ţàĝŢàĝ ḿüšţ àþþéàŕ îñ ţŕàñšļàţîöñ (éñƒöŕçéđ)
cloneableŢŕàñšļàţöŕ ḿàý đüþļîçàţé ţĥé ţàĝŢàĝ çöüñţ ḿüšţ ñöţ éẋçééđ šöüŕçé çöüñţ
reorderableŢŕàñšļàţöŕ ḿàý ŕéàŕŕàñĝé ţàĝ þöšîţîöñŢàĝ þöšîţîöñ ŕéļàţîṽé ţö öţĥéŕš îš ļöçķéđ

Üšîñĝ ṽöçàƃüļàŕîéš îñ à ƒöŕḿàţ ŕéàđéŕ

À ƒöŕḿàţ ŕéàđéŕ îñîţîàļîžéš à VocabularyRegistry àñđ üšéš îţ ţö þöþüļàţé îñļîñé-çöđé ḿéţàđàţà àš îţ ƃüîļđš à Ƃļöçķ'š []model.Run šéǫüéñçé:

package myformat

import "github.com/neokapi/neokapi/core/model"

type Reader struct {
vocab *model.VocabularyRegistry
}

func NewReader() *Reader {
vocab := model.NewVocabularyRegistry()
_ = vocab.LoadDefaults() // common-formatting + rich-html + rich-jsx + code-tokens
return &Reader{vocab: vocab}
}

Îñļîñé çöñţéñţ îš à ƒļàţ []model.Run (šéé Ƒ-02: Çöñţéñţ Ḿöđéļ). Àñ öþéñîñĝ ţàĝ ƃéçöḿéš à PcOpenRun, îţš ḿàţçĥîñĝ çļöšé à PcCloseRun ŵîţĥ ţĥé šàḿé ID, àñđ à šéļƒ-çļöšîñĝ çöñšţŕüçţ à PlaceholderRun. Ŵĥéñ ƃüîļđîñĝ öñé, ļööķ üþ ţĥé ṽöçàƃüļàŕý éñţŕý àñđ þöþüļàţé ţĥé ŕéñđéŕîñĝ àñđ çöñšţŕàîñţ ƒîéļđš, ḿîŕŕöŕîñĝ ţĥé þéŕ-ƒöŕḿàţ runBuilder ĥéļþéŕš (core/formats/*/run_builder.go):

// openRun builds the opening half of a paired code, e.g. <b> / <a href="…">.
func (r *Reader) openRun(semType, subType, id, nativeMarkup string) model.Run {
info := r.vocab.LookupOrFallback(semType)
return model.Run{PcOpen: &model.PcOpenRun{
ID: id, // shared with the matching PcClose
Type: semType, // "fmt:bold"
SubType: subType, // "html:b" or "md:strong"
Data: nativeMarkup, // original markup for roundtrip
Disp: info.Display.Open, // "[B]"
Equiv: info.Equiv, // "" (or "\n" for struct:break)
Constraints: &model.RunConstraints{
Deletable: info.Constraints.Deletable,
Cloneable: info.Constraints.Cloneable,
Reorderable: info.Constraints.Reorderable,
},
}}
}

// closeRun builds the matching close. PcCloseRun shares the opener's ID and
// replays its own native markup; it inherits the opener's constraints.
func (r *Reader) closeRun(semType, subType, id, nativeMarkup string) model.Run {
info := r.vocab.LookupOrFallback(semType)
return model.Run{PcClose: &model.PcCloseRun{
ID: id,
Type: semType,
SubType: subType,
Data: nativeMarkup, // "</b>"
Equiv: info.Equiv,
}}
}

// phRun builds a self-closing placeholder, e.g. <br/> or a variable token.
func (r *Reader) phRun(semType, subType, id, nativeMarkup string) model.Run {
info := r.vocab.LookupOrFallback(semType)
return model.Run{Ph: &model.PlaceholderRun{
ID: id,
Type: semType,
SubType: subType,
Data: nativeMarkup,
Disp: info.Display.Placeholder, // "[BR/]"
Equiv: info.Equiv, // "\n" for struct:break
Constraints: &model.RunConstraints{
Deletable: info.Constraints.Deletable,
Cloneable: info.Constraints.Cloneable,
Reorderable: info.Constraints.Reorderable,
},
}}
}

Ḿàþþîñĝ ñàţîṽé éļéḿéñţš ţö šéḿàñţîç ţýþéš

Éàçĥ ƒöŕḿàţ ḿàþš îţš ñàţîṽé çöñšţŕüçţš ţö šéḿàñţîç ţýþéš. Ţĥé ĤŢḾĻ ŕéàđéŕ ķéýš à ñàḿé → ţýþé ḿàþ öñ ţĥé éļéḿéñţ ñàḿé:

var htmlSemanticTypes = map[string]string{
"b": "fmt:bold", "strong": "fmt:bold",
"i": "fmt:italic", "em": "fmt:italic",
"u": "fmt:underline", "s": "fmt:strikethrough",
"a": "link:hyperlink", "code": "fmt:code",
"br": "struct:break", "img": "media:image",
"sub": "fmt:subscript", "sup": "fmt:superscript", "mark": "fmt:highlight",
}

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

Ḿàŕķđöŵñ çöñšţŕüçţŠéḿàñţîç ţýþé
šţŕöñĝ éḿþĥàšîš (ast.Emphasis ļéṽéļ 2)fmt:bold
éḿþĥàšîš (ast.Emphasis ļéṽéļ 1)fmt:italic
îñļîñé çöđéfmt:code
ļîñķlink:hyperlink
îḿàĝélink:image

À šöƒţ ļîñé ƃŕéàķ îš ñöţ à ŕüñ: îţ îš éḿîţţéđ àš îñļîñé ţéẋţ çöñţîñüàţîöñ (šéé softBreakContinuation), ñöţ à struct:break þļàçéĥöļđéŕ.

ŠüƃŢýþé çöñṽéñţîöñš

Ţĥé SubType ƒîéļđ ŕéçöŕđš ƒöŕḿàţ-šþéçîƒîç þŕöṽéñàñçé üšîñĝ à þŕéƒîẋ çöñṽéñţîöñ: html: (html:b, html:span), md: (md:strong), xlf: (xlf:var), docx: (docx:w:b). Çüšţöḿ ƒöŕḿàţš šĥöüļđ üšé à ŕéṽéŕšé-đöḿàîñ þŕéƒîẋ: com.acme:custom-tag.

Çŕéàţîñĝ à çüšţöḿ ṽöçàƃüļàŕý

1. Çŕéàţé ţĥé ĴŠÖÑ ƒîļé

Çŕéàţé à ĴŠÖÑ ƒîļé üñđéŕ core/model/vocabularies/:

{
"name": "my-domain",
"version": "1.0",
"extends": "common-formatting",
"types": {
"domain:widget": {
"category": "domain",
"label": "Widget",
"html": { "placeholder": "<span class=\"widget\"/>" },
"display": { "placeholder": "[WIDGET]" },
"chipLabel": { "placeholder": "wgt" },
"color": {
"bg": "rgba(168,85,247,0.15)",
"border": "rgba(168,85,247,0.5)",
"text": "rgb(168,85,247)"
},
"equiv": "",
"constraints": { "deletable": false, "cloneable": false, "reorderable": true }
}
}
}

2. Ļöàđ îţ îñţö ţĥé ŕéĝîšţŕý

LoadDefaults() ļöàđš ţĥé éḿƃéđđéđ ṽöçàƃüļàŕîéš. Ţö àđđ öñé àţ ŕüñţîḿé:

vocab := model.NewVocabularyRegistry()
vocab.LoadDefaults()

customData, _ := os.ReadFile("my-domain.json")
vocab.Load(customData)

3. Ḿàþ îţ îñ ýöüŕ ŕéàđéŕ

Àđđ ţĥé ñéŵ ţýþé ţö ýöüŕ ƒöŕḿàţ ŕéàđéŕ'š šéḿàñţîç ţýþé ḿàþþîñĝ:

var myFormatSemanticTypes = map[string]string{
"widget": "domain:widget",
}

ŠþàñÇļàššîƒý ţööļ

Ƒöŕ ƒöŕḿàţš ţĥàţ đö ñöţ þéŕƒöŕḿ ƒüļļ šéḿàñţîç çļàššîƒîçàţîöñ (ƒöŕ éẋàḿþļé, ŵĥéñ çöñţéñţ àŕŕîṽéš ṽîà ţĥé Okapi ƃŕîđĝé), ţĥé span-classify ţööļ ŕéçļàššîƒîéš ĝéñéŕîç code:markup îñļîñé-çöđé ŕüñš (Ph / PcOpen / PcClose) îñţö þŕöþéŕ šéḿàñţîç ţýþéš:

tool := tools.NewSpanClassifyTool(&tools.SpanClassifyConfig{})

Îţ àþþļîéš šţŕàţéĝîéš îñ öŕđéŕ: çĥéçķ ţĥé ŕüñ'š SubType àĝàîñšţ ķñöŵñ Okapi ţýþé šţŕîñĝš, þàŕšé Data ƒöŕ àñ ĤŢḾĻ éļéḿéñţ ñàḿé, ļööķ ţĥàţ ñàḿé üþ îñ ţĥé šéḿàñţîç ţýþé ḿàþ, àñđ öţĥéŕŵîšé ļéàṽé ţĥé ŕüñ àš code:markup. Ţĥé ţööļ ñàḿé îš ŕéţàîñéđ ƒöŕ ƃàçķŵàŕđš çöḿþàţîƃîļîţý ŵîţĥ éẋîšţîñĝ ƒļöŵ đéƒîñîţîöñš.

Ţéšţîñĝ ṽöçàƃüļàŕîéš

func TestMyVocabulary(t *testing.T) {
vocab := model.NewVocabularyRegistry()
require.NoError(t, vocab.LoadDefaults())

info := vocab.Lookup("fmt:bold")
require.NotNil(t, info)
assert.Equal(t, "formatting", info.Category)
assert.True(t, info.Constraints.Deletable)

unknown := vocab.LookupOrFallback("custom:unknown")
require.NotNil(t, unknown)
assert.True(t, unknown.Constraints.Deletable) // fallback rendering
}

Ƃéšţ þŕàçţîçéš

  1. Üšé éẋîšţîñĝ ţýþéš ŵĥéñ þöššîƃļé. Ḿàþ ţö fmt:bold ŕàţĥéŕ ţĥàñ çŕéàţîñĝ my-format:bold.
  2. Šéţ çöñšţŕàîñţš çöñšéŕṽàţîṽéļý. Ḿàŕķ çöđé ţöķéñš ñöñ-đéļéţàƃļé; ƒöŕḿàţţîñĝ ƒüļļý ƒļéẋîƃļé.
  3. Ķééþ ṽöçàƃüļàŕîéš šḿàļļ. Öñļý àđđ ţýþéš ŵîţĥ đîšţîñçţ ŕéñđéŕîñĝ öŕ çöñšţŕàîñţ ñééđš.
  4. Ţéšţ ŕöüñđţŕîþ ƒîđéļîţý. Ṽöçàƃüļàŕý ţýþéš àƒƒéçţ ŕéñđéŕîñĝ, ƃüţ éàçĥ ŕüñ'š Data đŕîṽéš öüţþüţ, šö ṽéŕîƒý ƃöţĥ.
  5. Éẋţéñđ ŕàţĥéŕ ţĥàñ ŕéþļàçé. Üšé extends ţö ƃüîļđ öñ common-formatting.

Ŕéļàţéđ ŕéàđîñĝ