- разбор документа по языку конвенций: шапка, области правил, блоки под метками - проверки формы правила, распространения, ссылок и самого suite.toml - словарь языка живёт в бинаре реестром «версия × естественный язык», темы и префиксы берутся только из манифеста
282 lines
8.9 KiB
Go
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, ", ")
|
|
}
|