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

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

À ṽöçàƃüļàŕý îš ţĥé šéţ öƒ šéḿàñţîç ţýþéš ţĥàţ çļàššîƒîéš îñļîñé çöđéš, šüçĥ àš fmt:bold öŕ link:hyperlink. Ýöü đéƒîñé öñé àš à ĴŠÖÑ ƒîļé, ļöàđ îţ îñţö à VocabularyRegistry, àñđ ḿàþ ýöüŕ ƒöŕḿàţ'š ñàţîṽé éļéḿéñţš ţö îţš ţýþéš. Ƒöŕ ŵĥàţ ṽöçàƃüļàŕîéš àŕé àñđ ŵĥý ţĥéý éẋîšţ, šéé Ṽöçàƃüļàŕîéš.

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

Éàçĥ ṽöçàƃüļàŕý îš à ĴŠÖÑ ƒîļé. Ţýþéš àŕé ķéýéđ ƃý à 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",
}

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

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

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

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

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

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.

Ŕéļàţéđ​