-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathste.go
More file actions
212 lines (185 loc) · 6.69 KB
/
Copy pathste.go
File metadata and controls
212 lines (185 loc) · 6.69 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
// Package ste validates text against ASD-STE100 Simplified Technical English.
//
// The package exposes a small stable API: build a Document from source text,
// configure a Validator with rules and an Env, and read the resulting
// Diagnostics. The CLI in cmd/ste is a thin wrapper over this API.
package ste
// Implements: SW-REQ-029
import (
"sort"
"github.com/probelabs/ste/internal/dictionary"
"github.com/probelabs/ste/internal/markup"
"github.com/probelabs/ste/internal/text"
)
// Implements: SYS-REQ-004, INT-REQ-001, SW-REQ-008
// Severity ranks a Diagnostic.
type Severity int
const (
// SeverityError is a hard STE violation.
SeverityError Severity = iota
// SeverityWarning is a likely violation or a heuristic finding.
SeverityWarning
// SeverityInfo is advisory.
SeverityInfo
)
// String returns the lowercase severity name used in machine output.
func (s Severity) String() string {
switch s {
case SeverityError:
return "error"
case SeverityWarning:
return "warning"
default:
return "info"
}
}
// SeverityFromString parses a severity name.
func SeverityFromString(s string) (Severity, bool) {
switch s {
case "error":
return SeverityError, true
case "warning":
return SeverityWarning, true
case "info":
return SeverityInfo, true
default:
return SeverityError, false
}
}
// Diagnostic is one rule finding with a 1-based source location.
type Diagnostic struct {
Rule string `json:"rule"`
Severity Severity `json:"-"`
Message string `json:"message"`
// Suggestion is an imperative fix when the rule knows one.
Suggestion string `json:"suggestion,omitempty"`
Line int `json:"line"`
Column int `json:"column"`
// Length is the span width in runes for caret highlighting (0 = 1).
Length int `json:"length,omitempty"`
}
// Word is one token with its 1-based source position.
type Word = text.Word
// Sentence is one sentence of source text.
type Sentence = text.Sentence
// Document is parsed source text ready for validation.
type Document struct {
Source string
Sentences []Sentence
}
// Parse splits plain source text into sentences.
func Parse(source string) *Document {
return &Document{Source: source, Sentences: text.Split(source, nil)}
}
// ParseSteps splits plain source text into sentences, marking sentences that
// start on one of the given 1-based lines as procedure steps. Hosts whose
// fields are procedure-shaped (acceptance-criterion text governed as one
// instruction per step) mark every line, which scopes the imperative rules
// the way a list-item document would. Positions refer to the source.
func ParseSteps(source string, stepLines map[int]bool) *Document {
return &Document{Source: source, Sentences: text.Split(source, stepLines)}
}
// ParseMarkdown strips markdown syntax, then splits into sentences. List
// items become procedure steps. Positions refer to the original source.
func ParseMarkdown(source string) *Document {
clean := markup.Clean(source)
return &Document{Source: source, Sentences: text.Split(clean.Text, clean.StepLines)}
}
// Dictionary is the approved-word dictionary type.
type Dictionary = dictionary.Dictionary
// Entry is one dictionary word: its part of speech, approval status, approved
// alternatives, and approved inflected forms. Hosts build entries in memory
// to overlay project vocabulary on a loaded dictionary; see
// Dictionary.MergeEntries.
type Entry = dictionary.Entry
// LoadDictionary reads a ste-dictionary/v1 JSON dictionary file.
func LoadDictionary(path string) (*Dictionary, error) {
return dictionary.Load(path)
}
// SampleDictionary returns the embedded sample dictionary for tests and demos.
func SampleDictionary() (*Dictionary, error) {
return dictionary.Sample()
}
// FullDictionary returns the embedded ASD-STE100 Issue 9 dictionary. It is
// the default dictionary of the CLI. See THIRD_PARTY_NOTICES.md for
// provenance and attribution.
func FullDictionary() (*Dictionary, error) {
return dictionary.Full()
}
// Options tune the rule catalog. The zero value is invalid; use
// DefaultOptions.
type Options struct {
// InstructionWords is the word limit for an instruction sentence.
InstructionWords int
// DescriptiveWords is the word limit for a descriptive sentence.
DescriptiveWords int
// NounClusterWords is the limit for consecutive nouns.
NounClusterWords int
// ParagraphSentences is the sentence limit for a descriptive paragraph.
ParagraphSentences int
// Units are the approved units of measurement.
Units []string
// AllowedPunctuation lists the approved punctuation marks.
AllowedPunctuation []rune
}
// DefaultOptions returns the ASD-STE100 default limits and lists.
func DefaultOptions() Options {
return Options{
InstructionWords: 20,
DescriptiveWords: 25,
NounClusterWords: 3,
ParagraphSentences: 10,
Units: []string{"mm", "cm", "m", "km", "kg", "g", "N", "kPa", "MPa",
"V", "A", "W", "°C", "min", "s", "h", "l", "ml"},
AllowedPunctuation: []rune{'.', ',', ':', '-', '\'', '(', ')', '"', '?', '/'},
}
}
// Env carries the run context every rule sees.
type Env struct {
Dict *Dictionary
Opts Options
}
// Rule is one deterministic ASD-STE100 checker. Rules register with the
// catalog in internal/rules; the engine executes every registered rule.
type Rule interface {
// ID is the stable rule identifier, for example "sentence-length".
ID() string
// Section is the ASD-STE100 writing-rule section the rule maps to.
Section() string
// Description is one STE sentence that states what the rule checks.
Description() string
// Check examines a Document. env.Dict may be nil; rules that need a
// dictionary must report nothing when it is nil.
Check(d *Document, env *Env) []Diagnostic
}
// Validator runs a set of rules over Documents.
type Validator struct {
env *Env
rules []Rule
}
// NewValidator builds a Validator. Rules execute in the order given.
func NewValidator(env *Env, rules ...Rule) *Validator {
return &Validator{env: env, rules: rules}
}
// Validate runs every registered rule and returns diagnostics sorted by
// source position, then rule id. The result is deterministic. Findings of the
// word-form rule family (approved-words, approved-form, ing-form) are folded
// to one per token: they describe a single defect — a word outside its
// approved dictionary entry — from three angles.
func (v *Validator) Validate(d *Document) []Diagnostic {
var diags []Diagnostic
for _, r := range v.rules {
diags = append(diags, r.Check(d, v.env)...)
}
diags = dedupeWordForms(diags)
sort.SliceStable(diags, func(i, j int) bool {
if diags[i].Line != diags[j].Line {
return diags[i].Line < diags[j].Line
}
if diags[i].Column != diags[j].Column {
return diags[i].Column < diags[j].Column
}
return diags[i].Rule < diags[j].Rule
})
return diags
}