комментарии и сообщения переведены на английский
- комментарии, тексты ошибок, вывод CLI и сообщения тестов теперь на английском - по-русски остались только литералы словаря ru и содержимое фикстур: это данные под проверкой, а не текст инструмента - согласование числительных в итоге упростилось до английского plural
This commit is contained in:
+77
-73
@@ -1,14 +1,15 @@
|
||||
// Package doc разбирает файл, записанный языком конвенций: шапку, правила и
|
||||
// их блоки.
|
||||
// Package doc parses a file written in the conventions language: its front
|
||||
// matter, its rules and their blocks.
|
||||
//
|
||||
// Модель разбора взята прямо из языка. Правило — заголовок вида
|
||||
// `### <ПРЕФИКС>-<номер>. <название>`; область правила тянется от заголовка до
|
||||
// следующего заголовка любого уровня. Внутри области текст принадлежит
|
||||
// последнему открытому блоку: метка блок открывает, и блок длится до следующей
|
||||
// метки или до конца области. Проза — то, что лежит вне областей правил.
|
||||
// The parsing model is taken straight from the language. A rule is a heading of
|
||||
// the form `### <PREFIX>-<number>. <title>`; the area of a rule runs from that
|
||||
// heading to the next heading of any level. Inside the area text belongs to the
|
||||
// last block opened: a mark opens a block, and the block lasts until the next
|
||||
// mark or until the end of the area. Prose is what lies outside rule areas.
|
||||
//
|
||||
// Границу считает разметка, а не суждение о том, где правило кончилось: ровно
|
||||
// поэтому проверка «модальных слов вне правил нет» вообще реализуема.
|
||||
// The boundary is counted by the markup rather than by a judgement about where
|
||||
// a rule ended: that is exactly what makes the "no modal words outside rules"
|
||||
// check implementable at all.
|
||||
package doc
|
||||
|
||||
import (
|
||||
@@ -21,62 +22,64 @@ import (
|
||||
"git.vakhrushev.me/av/convy/internal/lang"
|
||||
)
|
||||
|
||||
// Heading — заголовок любого уровня.
|
||||
// Heading is a heading of any level.
|
||||
type Heading struct {
|
||||
Level int
|
||||
Text string
|
||||
Line int
|
||||
}
|
||||
|
||||
// BlockKind различает блок нормы и блок под меткой.
|
||||
// BlockKind tells a norm block from a marked one.
|
||||
type BlockKind int
|
||||
|
||||
const (
|
||||
// Norm — блок нормы: открыт модальным словом.
|
||||
// Norm is a block of the norm, opened by a modal word.
|
||||
Norm BlockKind = iota + 1
|
||||
// Marked — блок под меткой: ПОЧЕМУ, ПРИМЕРЫ, СНЯТО, МЕХАНИЗИРОВАНО.
|
||||
// Marked is a block under a mark: rationale, examples, retired,
|
||||
// mechanized.
|
||||
Marked
|
||||
)
|
||||
|
||||
// Block — часть правила, открытая словом словаря в начале абзаца.
|
||||
// Block is a part of a rule opened by a vocabulary word at the start of a
|
||||
// paragraph.
|
||||
type Block struct {
|
||||
Kind BlockKind
|
||||
Word string
|
||||
Level lang.Level
|
||||
Mark lang.Mark
|
||||
// Start — строка, на которой стоит открывающая метка.
|
||||
// Start is the line the opening mark stands on.
|
||||
Start int
|
||||
// End — последняя строка блока: блок длится до следующей метки или до
|
||||
// конца области правила.
|
||||
// End is the last line of the block: a block lasts until the next mark
|
||||
// or until the end of the rule area.
|
||||
End int
|
||||
// Rest — текст абзаца после метки.
|
||||
// Rest is the text of the paragraph after the mark.
|
||||
Rest string
|
||||
}
|
||||
|
||||
// Rule — правило: заголовок с идентификатором и его область.
|
||||
// Rule is a rule: a heading carrying an identifier, plus its area.
|
||||
type Rule struct {
|
||||
Prefix string
|
||||
Num int
|
||||
Title string
|
||||
// Line — строка заголовка.
|
||||
// Line is the line of the heading.
|
||||
Line int
|
||||
// HeadingLevel — уровень заголовка; каноническая форма — третий.
|
||||
// HeadingLevel is the level of the heading; the canonical form is three.
|
||||
HeadingLevel int
|
||||
// Malformed — заголовок опознан как правило, но записан не по форме
|
||||
// `### <ПРЕФИКС>-<номер>. <название>`.
|
||||
// Malformed is set when a heading was recognized as a rule but is not
|
||||
// written in the form `### <PREFIX>-<number>. <title>`.
|
||||
Malformed string
|
||||
// Start, End — область правила: от строки после заголовка до строки
|
||||
// перед следующим заголовком включительно.
|
||||
// Start and End bound the rule area: from the line after the heading to
|
||||
// the line before the next heading, inclusive.
|
||||
Start, End int
|
||||
Blocks []Block
|
||||
}
|
||||
|
||||
// ID возвращает идентификатор правила.
|
||||
// ID returns the identifier of the rule.
|
||||
func (r Rule) ID() string {
|
||||
return fmt.Sprintf("%s-%d", r.Prefix, r.Num)
|
||||
}
|
||||
|
||||
// Block ищет первый блок под указанной меткой.
|
||||
// Block finds the first block under the given mark.
|
||||
func (r Rule) Block(m lang.Mark) (Block, bool) {
|
||||
for _, b := range r.Blocks {
|
||||
if b.Kind == Marked && b.Mark == m {
|
||||
@@ -86,9 +89,9 @@ func (r Rule) Block(m lang.Mark) (Block, bool) {
|
||||
return Block{}, false
|
||||
}
|
||||
|
||||
// Norms перечисляет блоки нормы. Их должно быть ровно ноль (у снятого
|
||||
// правила) или один: норма — одна фраза, две нормы под одним номером нечем
|
||||
// адресовать по отдельности.
|
||||
// Norms lists the norm blocks. There must be exactly zero of them (for a
|
||||
// retired rule) or one: a norm is a single statement, and two norms under one
|
||||
// number leave no way to address either on its own.
|
||||
func (r Rule) Norms() []Block {
|
||||
var out []Block
|
||||
for _, b := range r.Blocks {
|
||||
@@ -99,25 +102,25 @@ func (r Rule) Norms() []Block {
|
||||
return out
|
||||
}
|
||||
|
||||
// Paragraph — абзац: строки между пустыми.
|
||||
// Paragraph is a paragraph: the lines between blank ones.
|
||||
type Paragraph struct {
|
||||
Start, End int
|
||||
Lines []string
|
||||
}
|
||||
|
||||
// Text склеивает абзац в одну строку.
|
||||
// Text joins the paragraph into a single string.
|
||||
func (p Paragraph) Text() string {
|
||||
return strings.Join(p.Lines, " ")
|
||||
}
|
||||
|
||||
// Document — разобранный файл.
|
||||
// Document is a parsed file.
|
||||
type Document struct {
|
||||
// Path — путь от корня набора, в форме со слэшами.
|
||||
// Path is the path from the root of the suite, in slash form.
|
||||
Path string
|
||||
Front Front
|
||||
lines []string
|
||||
fence []bool
|
||||
// Body — первая строка тела, после шапки.
|
||||
// Body is the first line of the body, past the front matter.
|
||||
Body int
|
||||
Headings []Heading
|
||||
Rules []Rule
|
||||
@@ -125,15 +128,15 @@ type Document struct {
|
||||
|
||||
var (
|
||||
headingRe = regexp.MustCompile(`^(#{1,6})\s+(.*)$`)
|
||||
// ruleHeadRe ловит заголовок, начинающийся с идентификатора правила, —
|
||||
// в том числе записанный не по форме: иначе опечатка в заголовке
|
||||
// превратила бы правило в прозу и молча исчезла из нумерации.
|
||||
// ruleHeadRe catches a heading that starts with a rule identifier —
|
||||
// including one written out of form: otherwise a typo in a heading would
|
||||
// turn a rule into prose and vanish from the numbering unnoticed.
|
||||
ruleHeadRe = regexp.MustCompile(`^([A-Z]{4})-(\d+)(.*)$`)
|
||||
fenceRe = regexp.MustCompile("^\\s*(`{3,}|~{3,})")
|
||||
)
|
||||
|
||||
// Load читает и разбирает файл. path — путь от корня набора, name — путь в
|
||||
// файловой системе.
|
||||
// Load reads and parses a file. path is the path from the root of the suite,
|
||||
// name the path in the file system.
|
||||
func Load(path, name string) (*Document, error) {
|
||||
data, err := os.ReadFile(name)
|
||||
if err != nil {
|
||||
@@ -142,7 +145,7 @@ func Load(path, name string) (*Document, error) {
|
||||
return Parse(path, string(data))
|
||||
}
|
||||
|
||||
// Parse разбирает содержимое файла.
|
||||
// Parse parses the contents of a file.
|
||||
func Parse(path, content string) (*Document, error) {
|
||||
d := &Document{Path: path}
|
||||
d.lines = strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||
@@ -151,7 +154,7 @@ func Parse(path, content string) (*Document, error) {
|
||||
d.Front = front
|
||||
d.Body = body
|
||||
if err != nil {
|
||||
return d, fmt.Errorf("%s: шапка: %w", path, err)
|
||||
return d, fmt.Errorf("%s: front matter: %w", path, err)
|
||||
}
|
||||
|
||||
d.markFences()
|
||||
@@ -160,10 +163,10 @@ func Parse(path, content string) (*Document, error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// markFences отмечает строки внутри огороженных блоков кода. Всё, что
|
||||
// проверяется разбором текста, эти строки пропускает: пример на SQL с
|
||||
// заглавным WHEN не делает набор двуязычным, а `### XKEY-5` из примера в
|
||||
// документации — не правило.
|
||||
// markFences marks the lines inside fenced code blocks. Everything checked by
|
||||
// parsing text skips them: a SQL sample with an uppercase WHEN does not make
|
||||
// the suite bilingual, and a `### XKEY-5` inside a documentation sample is not
|
||||
// a rule.
|
||||
func (d *Document) markFences() {
|
||||
d.fence = make([]bool, len(d.lines))
|
||||
open := ""
|
||||
@@ -217,7 +220,7 @@ func (d *Document) collectRules() {
|
||||
Line: h.Line,
|
||||
HeadingLevel: h.Level,
|
||||
Start: h.Line + 1,
|
||||
End: d.lineCount(),
|
||||
End: d.Len(),
|
||||
}
|
||||
if i+1 < len(d.Headings) {
|
||||
rule.End = d.Headings[i+1].Line - 1
|
||||
@@ -228,19 +231,20 @@ func (d *Document) collectRules() {
|
||||
case strings.HasPrefix(tail, ". "):
|
||||
rule.Title = strings.TrimSpace(tail[2:])
|
||||
case tail == "":
|
||||
rule.Malformed = "у заголовка правила нет названия"
|
||||
rule.Malformed = "the rule heading has no title"
|
||||
case strings.HasPrefix(tail, "."):
|
||||
rule.Title = strings.TrimSpace(tail[1:])
|
||||
default:
|
||||
rule.Malformed = "после идентификатора в заголовке нет точки"
|
||||
rule.Malformed = "no period after the identifier in the heading"
|
||||
rule.Title = strings.TrimSpace(tail)
|
||||
}
|
||||
d.Rules = append(d.Rules, rule)
|
||||
}
|
||||
}
|
||||
|
||||
// Blocks размечает области правил по словарю набора. Разметка отложена до
|
||||
// загрузки манифеста: до неё неизвестно, каким словарём записан набор.
|
||||
// Blocks marks up the rule areas using the suite's vocabulary. The markup is
|
||||
// deferred until the manifest is loaded: before that it is unknown which
|
||||
// vocabulary the suite is written in.
|
||||
func (d *Document) Blocks(v lang.Vocabulary) {
|
||||
for i := range d.Rules {
|
||||
r := &d.Rules[i]
|
||||
@@ -269,9 +273,9 @@ func (d *Document) Blocks(v lang.Vocabulary) {
|
||||
}
|
||||
}
|
||||
|
||||
// boldLead выделяет содержимое первого полужирного участка, если абзац с него
|
||||
// начинается. Метка стоит первой в своём абзаце, полужирным и с точкой —
|
||||
// именно этим она отличается от упоминания ступени в середине фразы.
|
||||
// boldLead extracts the contents of the first bold span if the paragraph opens
|
||||
// with one. A mark stands first in its paragraph, in bold and with a period —
|
||||
// that is exactly what tells it from a mention of a step mid-sentence.
|
||||
func boldLead(line string) (bold, rest string, ok bool) {
|
||||
line = strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(line, "**") {
|
||||
@@ -284,13 +288,13 @@ func boldLead(line string) (bold, rest string, ok bool) {
|
||||
return line[2 : 2+end], line[2+end+2:], true
|
||||
}
|
||||
|
||||
// Paragraphs режет диапазон строк на абзацы. Границы включительные, нумерация
|
||||
// с единицы. Строки внутри огороженных блоков в абзацы не попадают: код —
|
||||
// иллюстрация, а не текст правила.
|
||||
// Paragraphs cuts a range of lines into paragraphs. Bounds are inclusive and
|
||||
// numbering starts at one. Lines inside fenced blocks do not enter paragraphs:
|
||||
// code is an illustration, not the text of a rule.
|
||||
func (d *Document) Paragraphs(from, to int) []Paragraph {
|
||||
var out []Paragraph
|
||||
var cur *Paragraph
|
||||
for n := max(from, 1); n <= min(to, d.lineCount()); n++ {
|
||||
for n := max(from, 1); n <= min(to, d.Len()); n++ {
|
||||
line := d.lines[n-1]
|
||||
if d.fence[n-1] || strings.TrimSpace(line) == "" {
|
||||
cur = nil
|
||||
@@ -306,15 +310,16 @@ func (d *Document) Paragraphs(from, to int) []Paragraph {
|
||||
return out
|
||||
}
|
||||
|
||||
// Preamble возвращает границы вводной прозы: от тела до первого правила.
|
||||
// Preamble returns the bounds of the introductory prose: from the body to the
|
||||
// first rule.
|
||||
func (d *Document) Preamble() (from, to int) {
|
||||
if len(d.Rules) == 0 {
|
||||
return d.Body, d.lineCount()
|
||||
return d.Body, d.Len()
|
||||
}
|
||||
return d.Body, d.Rules[0].Line - 1
|
||||
}
|
||||
|
||||
// InRule отвечает, лежит ли строка внутри области какого-нибудь правила.
|
||||
// InRule reports whether a line lies inside the area of some rule.
|
||||
func (d *Document) InRule(n int) bool {
|
||||
for _, r := range d.Rules {
|
||||
if n >= r.Line && n <= r.End {
|
||||
@@ -324,24 +329,25 @@ func (d *Document) InRule(n int) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Line возвращает строку с номером n.
|
||||
// Line returns line number n.
|
||||
func (d *Document) Line(n int) string {
|
||||
if n < 1 || n > d.lineCount() {
|
||||
if n < 1 || n > d.Len() {
|
||||
return ""
|
||||
}
|
||||
return d.lines[n-1]
|
||||
}
|
||||
|
||||
// Fenced отвечает, лежит ли строка внутри огороженного блока кода.
|
||||
// Fenced reports whether a line lies inside a fenced code block.
|
||||
func (d *Document) Fenced(n int) bool {
|
||||
return n >= 1 && n <= d.lineCount() && d.fence[n-1]
|
||||
return n >= 1 && n <= d.Len() && d.fence[n-1]
|
||||
}
|
||||
|
||||
// Prose проходит строки тела, не попавшие в огороженные блоки, и отдаёт их с
|
||||
// вырезанным содержимым инлайн-кода. В бэктиках идентификатор стоит как
|
||||
// пример записи, а не как ссылка, — различает их именно разметка.
|
||||
// Prose walks the lines of the body that did not land in fenced blocks and
|
||||
// hands them over with the contents of inline code cut out. Inside backticks an
|
||||
// identifier stands as a sample of the notation rather than as a reference —
|
||||
// and it is the markup that tells the two apart.
|
||||
func (d *Document) Prose(yield func(n int, text string) bool) {
|
||||
for n := d.Body; n <= d.lineCount(); n++ {
|
||||
for n := d.Body; n <= d.Len(); n++ {
|
||||
if d.fence[n-1] {
|
||||
continue
|
||||
}
|
||||
@@ -351,7 +357,7 @@ func (d *Document) Prose(yield func(n int, text string) bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// StripInline вырезает содержимое инлайн-кода, оставляя разделители.
|
||||
// StripInline cuts out the contents of inline code, keeping the delimiters.
|
||||
func StripInline(line string) string {
|
||||
var b strings.Builder
|
||||
inCode := false
|
||||
@@ -370,7 +376,5 @@ func StripInline(line string) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Len возвращает число строк в файле.
|
||||
// Len returns the number of lines in the file.
|
||||
func (d *Document) Len() int { return len(d.lines) }
|
||||
|
||||
func (d *Document) lineCount() int { return len(d.lines) }
|
||||
|
||||
Reference in New Issue
Block a user