// Package lang holds the vocabularies of the conventions language: the words // that spell out a rule's modality, the marks of its blocks, and the // connectives of a scenario block. // // A vocabulary belongs to a language version and to the suite's natural // language, not to the suite itself: version 1 in Russian names the same words // in every repository, and repeating that list in every manifest buys nothing. // While the language specification lives together with the canon, the // vocabularies live here; once it moves to its own repository with vocabulary // files of its own, those become the source, and the Vocabulary type together // with every check built on it stays as it is. package lang import ( "fmt" "sort" "strings" "unicode" "unicode/utf8" ) // Level is a step on the scale of obligation. There are five steps in the four // categories of ISO/IEC Directives, Part 2; which words name them is a // parameter of the natural language, while the scale is one for all // vocabularies. type Level int const ( Requirement Level = iota + 1 Prohibition Recommendation RecommendationAgainst Permission ) // String names the step for diagnostics — the role, not the vocabulary word. func (l Level) String() string { switch l { case Requirement: return "requirement" case Prohibition: return "prohibition" case Recommendation: return "recommendation" case RecommendationAgainst: return "recommendation against" case Permission: return "permission" } return "unknown level" } // Mark labels a block of a rule. Marks set no obligation, they only say what // this is: a rationale, an illustration, a note about mechanization, a stub in // place of a retired rule. type Mark int const ( Rationale Mark = iota + 1 Examples Mechanized Retired ) func (m Mark) String() string { switch m { case Rationale: return "rationale" case Examples: return "examples" case Mechanized: return "mechanized" case Retired: return "retired" } return "unknown mark" } // Connective is a service word of a scenario block. Such words stay out of the // language version line and out of the "modal words outside rules" check: they // set no obligation, only structure. type Connective int const ( When Connective = iota + 1 Then And Or ) // Vocabulary is the vocabulary of one language version in one natural language. type Vocabulary struct { Version int Code string Modals map[string]Level Marks map[string]Mark Scenario map[string]Connective } // Modal reports the step of a word if the word belongs to this scale. func (v Vocabulary) Modal(word string) (Level, bool) { l, ok := v.Modals[word] return l, ok } // Mark reports the role of a mark if the word belongs to these marks. func (v Vocabulary) Mark(word string) (Mark, bool) { m, ok := v.Marks[word] return m, ok } // Word returns the word this vocabulary uses for a step. func (v Vocabulary) Word(l Level) string { for w, got := range v.Modals { if got == l { return w } } return "" } // MarkWord returns the word this vocabulary uses for a mark. func (v Vocabulary) MarkWord(m Mark) string { for w, got := range v.Marks { if got == m { return w } } return "" } // Lead returns the vocabulary word that opens text. The longest match wins: // "MUST NOT" must not be read as "MUST", and a mark carrying a date // ("RETIRED 2026-07-26") must not be read as a different mark. 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 lists every word the language names in its version line: the steps of // the scale and the marks. Scenario connectives are not among them. 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 reports whether text starts with word w and the word ends // there: "MUSTARD" is not the word "MUST". 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 holds the vocabularies known to the binary. The outer key is the // language version, the inner one the code of the suite's natural language. 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 returns the vocabulary of a language version in the given natural // language. func Lookup(version int, code string) (Vocabulary, error) { byCode, ok := registry[version] if !ok { return Vocabulary{}, fmt.Errorf("language version %d is unknown to the tool; known versions: %s", version, versions()) } v, ok := byCode[code] if !ok { return Vocabulary{}, fmt.Errorf("vocabulary %q of language version %d is unknown to the tool; known: %s", code, version, codes(version)) } return v, nil } // Foreign lists the words of the other vocabularies of the same version — the // ones that give away a mixture of vocabularies. Words that coincide with the // suite's own are dropped. 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, ", ") }