- suite retire снимает правило, конвенцию или тему: правило остаётся заглушкой с датой и причиной, имя уезжает в раздел выбывших, снятие с непогашенными ссылками отклоняется с перечнем мест - добавлена проверка META-30: короткое описание языка обязано называть все слова словаря, иначе читатель копии толкует их по памяти - retired-раздел манифеста больше не считается объявлением пути: снятый префикс означает, что файл ушёл вместе с ним
178 lines
5.0 KiB
Go
178 lines
5.0 KiB
Go
// Package suite assembles a conventions suite in memory: its manifest, the
|
|
// vocabulary of its language, and the documents the language employs.
|
|
//
|
|
// What is subject to checking is everything the language employs: the
|
|
// convention files and the document the suite governs itself by. The list of
|
|
// those files comes from the manifest, from the section of live prefixes where
|
|
// every prefix carries a path. A file the language merely quotes — the
|
|
// description of the language itself — is not listed in the suite and gets no
|
|
// checks.
|
|
package suite
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.vakhrushev.me/av/convy/internal/doc"
|
|
"git.vakhrushev.me/av/convy/internal/lang"
|
|
"git.vakhrushev.me/av/convy/internal/manifest"
|
|
)
|
|
|
|
// Suite is a loaded suite.
|
|
type Suite struct {
|
|
Root string
|
|
Manifest *manifest.Manifest
|
|
Vocab lang.Vocabulary
|
|
|
|
// Docs holds the documents of the suite in the order of live prefixes.
|
|
Docs []*doc.Document
|
|
// ByPrefix maps a manifest prefix to its document.
|
|
ByPrefix map[string]*doc.Document
|
|
// Missing lists the prefixes whose file the manifest declares while the
|
|
// file system holds none.
|
|
Missing map[string]string
|
|
// Unregistered lists files found in the suite carrying front matter that
|
|
// the manifest does not declare. Such a file is neither checked nor
|
|
// assembled: for the suite it does not exist, however its author sees it.
|
|
Unregistered []string
|
|
// Broken lists files whose front matter could not be parsed.
|
|
Broken []error
|
|
}
|
|
|
|
// Load reads a suite from directory root.
|
|
func Load(root string) (*Suite, error) {
|
|
m, err := manifest.Load(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
vocab, err := lang.Lookup(m.Language.Version, m.Language.Lang)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", m.Path, err)
|
|
}
|
|
|
|
s := &Suite{
|
|
Root: root,
|
|
Manifest: m,
|
|
Vocab: vocab,
|
|
ByPrefix: make(map[string]*doc.Document),
|
|
Missing: make(map[string]string),
|
|
}
|
|
|
|
for _, prefix := range m.LivePrefixes() {
|
|
rel := filepath.ToSlash(m.Prefixes.Live[prefix])
|
|
name := filepath.Join(root, filepath.FromSlash(rel))
|
|
d, err := doc.Load(rel, name)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
s.Missing[prefix] = rel
|
|
continue
|
|
}
|
|
s.Broken = append(s.Broken, err)
|
|
continue
|
|
}
|
|
d.Blocks(vocab)
|
|
s.Docs = append(s.Docs, d)
|
|
s.ByPrefix[prefix] = d
|
|
}
|
|
|
|
if err := s.findUnregistered(); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// findUnregistered walks the suite looking for files written in the conventions
|
|
// language yet absent from the manifest. The marker is the prefix key in the
|
|
// front matter rather than the location of the file: the suite rearranges its
|
|
// taxonomy, while the front matter asserts.
|
|
func (s *Suite) findUnregistered() error {
|
|
// Only live prefixes declare a path. A retired entry records why a prefix
|
|
// left and when, not where a file lies — retiring a prefix means the file
|
|
// went with it, so one left behind is undeclared and has to say so.
|
|
declared := make(map[string]bool)
|
|
for _, path := range s.Manifest.Prefixes.Live {
|
|
declared[filepath.ToSlash(path)] = true
|
|
}
|
|
|
|
err := filepath.WalkDir(s.Root, func(name string, entry fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if entry.IsDir() {
|
|
if strings.HasPrefix(entry.Name(), ".") && name != s.Root {
|
|
return fs.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if filepath.Ext(entry.Name()) != ".md" {
|
|
return nil
|
|
}
|
|
rel, err := filepath.Rel(s.Root, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
if declared[rel] {
|
|
return nil
|
|
}
|
|
d, err := doc.Load(rel, name)
|
|
if err != nil {
|
|
// The file carries no front matter, or it is broken — for the
|
|
// suite this is not a document of the language but plain
|
|
// markdown lying nearby.
|
|
return nil
|
|
}
|
|
if d.Front.Prefix != "" {
|
|
s.Unregistered = append(s.Unregistered, rel)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("walking the suite: %w", err)
|
|
}
|
|
sort.Strings(s.Unregistered)
|
|
return nil
|
|
}
|
|
|
|
// Conventions picks out the convention documents — the ones that carry a topic
|
|
// and therefore travel to a consumer. A document without a topic, the one the
|
|
// suite governs itself by, gets no spread checks.
|
|
func (s *Suite) Conventions() []*doc.Document {
|
|
var out []*doc.Document
|
|
for _, d := range s.Docs {
|
|
if d.Front.Topic != "" {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Layers lists the layers of a topic — the documents that declared that name in
|
|
// their front matter.
|
|
func (s *Suite) Layers(topic string) []*doc.Document {
|
|
var out []*doc.Document
|
|
for _, d := range s.Docs {
|
|
if d.Front.Topic == topic {
|
|
out = append(out, d)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Prefix returns the prefix the manifest declares for a document.
|
|
func (s *Suite) Prefix(d *doc.Document) string {
|
|
prefix, _ := s.Manifest.PrefixOf(d.Path)
|
|
return prefix
|
|
}
|
|
|
|
// Exists reports whether the suite holds a file at a path from its root.
|
|
func (s *Suite) Exists(rel string) bool {
|
|
_, err := os.Stat(filepath.Join(s.Root, filepath.FromSlash(rel)))
|
|
return err == nil
|
|
}
|