Files
convy/internal/lang/vocabulary.go
T
av ccf046fb7b suite check: реализована проверка целостности набора
- разбор документа по языку конвенций: шапка, области правил, блоки под метками
- проверки формы правила, распространения, ссылок и самого suite.toml
- словарь языка живёт в бинаре реестром «версия × естественный язык», темы и
  префиксы берутся только из манифеста
2026-07-27 09:53:07 +03:00

282 lines
8.9 KiB
Go

// Package lang держит словари языка конвенций: слова, которыми записаны
// модальность правила, метки его блоков и связки сценарного блока.
//
// Словарь — свойство версии языка и естественного языка набора, а не самого
// набора: версия 1 по-русски задаёт один и тот же список слов в любом
// репозитории, и повторять его в каждом манифесте незачем. Пока спецификация
// языка живёт вместе с каноном, словари лежат здесь; когда она уедет в
// отдельный репозиторий со своими файлами словарей, источником станут они, а
// форма Vocabulary и все проверки поверх неё останутся прежними.
package lang
import (
"fmt"
"sort"
"strings"
"unicode"
"unicode/utf8"
)
// Level — ступень шкалы обязательности. Ступеней пять в четырёх категориях
// ISO/IEC Directives, Part 2; какими словами они названы — параметр
// естественного языка, а сама шкала одна на все словари.
type Level int
const (
Requirement Level = iota + 1
Prohibition
Recommendation
RecommendationAgainst
Permission
)
// String даёт имя ступени для сообщений об ошибках — не слово словаря, а роль.
func (l Level) String() string {
switch l {
case Requirement:
return "требование"
case Prohibition:
return "запрет"
case Recommendation:
return "рекомендация"
case RecommendationAgainst:
return "рекомендация против"
case Permission:
return "разрешение"
}
return "неизвестная ступень"
}
// Mark — метка блока правила. Метки обязательности не задают, а размечают:
// что здесь обоснование, что иллюстрация, что запись о механизации, что
// заглушка на месте снятого правила.
type Mark int
const (
Rationale Mark = iota + 1
Examples
Mechanized
Retired
)
func (m Mark) String() string {
switch m {
case Rationale:
return "обоснование"
case Examples:
return "примеры"
case Mechanized:
return "механизация"
case Retired:
return "снятое правило"
}
return "неизвестная метка"
}
// Connective — служебное слово сценарного блока. В строку о версии языка эти
// слова не входят и под проверку «модальные слова вне правил» не подпадают:
// обязательности они не задают, только структуру.
type Connective int
const (
When Connective = iota + 1
Then
And
Or
)
// Vocabulary — словарь одной версии языка на одном естественном языке.
type Vocabulary struct {
Version int
Code string
Modals map[string]Level
Marks map[string]Mark
Scenario map[string]Connective
}
// Modal сообщает ступень слова, если слово принадлежит шкале этого словаря.
func (v Vocabulary) Modal(word string) (Level, bool) {
l, ok := v.Modals[word]
return l, ok
}
// Mark сообщает роль метки, если слово принадлежит меткам этого словаря.
func (v Vocabulary) Mark(word string) (Mark, bool) {
m, ok := v.Marks[word]
return m, ok
}
// Word возвращает слово, которым в этом словаре записана ступень.
func (v Vocabulary) Word(l Level) string {
for w, got := range v.Modals {
if got == l {
return w
}
}
return ""
}
// MarkWord возвращает слово, которым в этом словаре записана метка.
func (v Vocabulary) MarkWord(m Mark) string {
for w, got := range v.Marks {
if got == m {
return w
}
}
return ""
}
// Lead возвращает слово словаря, которым начинается text, и его длину.
// Длиннейшее совпадение выигрывает: «НЕ ДОЛЖЕН» не должен читаться как
// «ДОЛЖЕН», а метка с датой («СНЯТО 2026-07-26») — как метка без неё.
func (v Vocabulary) Lead(text string) (string, bool) {
best := ""
for _, w := range v.Words() {
if len(w) <= len(best) {
continue
}
if !hasWordPrefix(text, w) {
continue
}
best = w
}
return best, best != ""
}
// Words перечисляет все слова словаря, которые язык объявляет в строке о
// версии: ступени шкалы и метки. Связки сценария сюда не входят.
func (v Vocabulary) Words() []string {
words := make([]string, 0, len(v.Modals)+len(v.Marks))
for w := range v.Modals {
words = append(words, w)
}
for w := range v.Marks {
words = append(words, w)
}
sort.Strings(words)
return words
}
// hasWordPrefix проверяет, что text начинается со слова w и слово на этом
// кончается: «ДОЛЖЕНСТВОВАНИЕ» словом ДОЛЖЕН не является.
func hasWordPrefix(text, w string) bool {
if !strings.HasPrefix(text, w) {
return false
}
rest := text[len(w):]
if rest == "" {
return true
}
r, _ := utf8.DecodeRuneInString(rest)
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
}
// registry — словари, известные бинарю. Ключ верхнего уровня — версия языка,
// вложенный — код естественного языка набора.
var registry = map[int]map[string]Vocabulary{
1: {
"ru": {
Version: 1,
Code: "ru",
Modals: map[string]Level{
"ДОЛЖЕН": Requirement,
"НЕ ДОЛЖЕН": Prohibition,
"СЛЕДУЕТ": Recommendation,
"НЕ СЛЕДУЕТ": RecommendationAgainst,
"ДОПУСКАЕТСЯ": Permission,
},
Marks: map[string]Mark{
"ПОЧЕМУ": Rationale,
"ПРИМЕРЫ": Examples,
"МЕХАНИЗИРОВАНО": Mechanized,
"СНЯТО": Retired,
},
Scenario: map[string]Connective{
"КОГДА": When,
"ТОГДА": Then,
"И": And,
"ИЛИ": Or,
},
},
"en": {
Version: 1,
Code: "en",
Modals: map[string]Level{
"MUST": Requirement,
"MUST NOT": Prohibition,
"SHOULD": Recommendation,
"SHOULD NOT": RecommendationAgainst,
"MAY": Permission,
},
Marks: map[string]Mark{
"WHY": Rationale,
"EXAMPLES": Examples,
"MECHANIZED": Mechanized,
"RETIRED": Retired,
},
Scenario: map[string]Connective{
"WHEN": When,
"THEN": Then,
"AND": And,
"OR": Or,
},
},
},
}
// Lookup выдаёт словарь версии языка на указанном естественном языке.
func Lookup(version int, code string) (Vocabulary, error) {
byCode, ok := registry[version]
if !ok {
return Vocabulary{}, fmt.Errorf("версия языка %d инструменту неизвестна, известны: %s", version, versions())
}
v, ok := byCode[code]
if !ok {
return Vocabulary{}, fmt.Errorf("словарь %q для версии языка %d инструменту неизвестен, известны: %s", code, version, codes(version))
}
return v, nil
}
// Foreign перечисляет слова чужих словарей той же версии — те, по которым
// видно смесь словарей. Слова, совпадающие с собственными, отброшены.
func Foreign(version int, code string) map[string]string {
own, err := Lookup(version, code)
if err != nil {
return nil
}
mine := make(map[string]bool)
for _, w := range own.Words() {
mine[w] = true
}
foreign := make(map[string]string)
for otherCode, other := range registry[version] {
if otherCode == code {
continue
}
for _, w := range other.Words() {
if !mine[w] {
foreign[w] = otherCode
}
}
}
return foreign
}
func versions() string {
out := make([]string, 0, len(registry))
for v := range registry {
out = append(out, fmt.Sprint(v))
}
sort.Strings(out)
return strings.Join(out, ", ")
}
func codes(version int) string {
out := make([]string, 0, len(registry[version]))
for c := range registry[version] {
out = append(out, c)
}
sort.Strings(out)
return strings.Join(out, ", ")
}