@ttsc/evidence 0.30.3 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/lib/index.d.ts +2 -3
- package/lib/index.js +2 -3
- package/lib/internal/canonicalDigest.js +12 -6
- package/lib/internal/loadPrismaModels.js +52 -7
- package/lib/internal/loadSwaggerOperations.js +253 -59
- package/lib/internal/normalizeSwaggerDocument.js +320 -2
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +16 -8
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +8 -2
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +10 -4
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +6 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +7 -1
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +11 -3
- package/lib/structures/ITtscEvidenceRules.d.ts +93 -0
- package/lib/structures/TtscEvidenceGraphMarkdownSymbol.d.ts +39 -0
- package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.d.ts +6 -0
- package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.d.ts +6 -0
- package/lib/structures/index.d.ts +4 -0
- package/lib/structures/index.js +4 -0
- package/native/base.go +25 -23
- package/native/bridge_trace.go +348 -0
- package/native/comments.go +1 -1
- package/native/config.go +6 -3
- package/native/declaration.go +17 -43
- package/native/documentation_regions.go +214 -0
- package/native/documented.go +5 -4
- package/native/exports.go +1 -1
- package/native/fingerprint.go +2 -8
- package/native/glob.go +4 -1
- package/native/graph.go +59 -23
- package/native/graph_nodes.go +2 -1
- package/native/hints.go +22 -18
- package/native/imports.go +7 -6
- package/native/input_reader.go +83 -0
- package/native/inputs.go +13 -12
- package/native/loader.go +36 -24
- package/native/markdown.go +587 -177
- package/native/markdown_capture.go +223 -0
- package/native/model.go +2 -1
- package/native/prisma.go +110 -25
- package/native/prisma_cache.go +12 -8
- package/native/prisma_locator.go +70 -33
- package/native/review.go +8 -7
- package/native/rooted_typescript.go +3 -4
- package/native/swagger.go +123 -15
- package/native/swagger_cache.go +16 -12
- package/native/todo.go +12 -0
- package/native/typescript.go +129 -25
- package/package.json +9 -7
- package/lib/typings/ITtscLintContributorRules.d.ts +0 -57
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +0 -20
- package/lib/typings/ITtscLintRuleOptionsMap.js +0 -2
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +0 -32
- package/lib/typings/index.d.ts +0 -5
- package/lib/typings/index.js +0 -21
- /package/lib/{typings/ITtscLintContributorRules.js → structures/ITtscEvidenceRules.js} +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphMarkdownSymbol.js +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphPrismaSymbol.js +0 -0
- /package/lib/{typings → structures}/TtscEvidenceGraphTypeScriptSymbol.js +0 -0
package/native/markdown.go
CHANGED
|
@@ -2,16 +2,478 @@ package evidence
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"io/fs"
|
|
5
|
-
"os"
|
|
6
5
|
"path/filepath"
|
|
7
6
|
"regexp"
|
|
7
|
+
"sort"
|
|
8
8
|
"strings"
|
|
9
9
|
"unicode"
|
|
10
|
+
|
|
11
|
+
"github.com/yuin/goldmark/v2/ast"
|
|
12
|
+
"github.com/yuin/goldmark/v2/parser"
|
|
13
|
+
"github.com/yuin/goldmark/v2/text"
|
|
14
|
+
"github.com/yuin/goldmark/v2/util"
|
|
10
15
|
)
|
|
11
16
|
|
|
12
|
-
var markdownCommentPattern = regexp.MustCompile(`(?s)<!--(.*?)-->`)
|
|
13
17
|
var explicitAnchorPattern = regexp.MustCompile(`\s*\{#([A-Za-z0-9][A-Za-z0-9._:-]*)\}\s*$`)
|
|
14
18
|
|
|
19
|
+
// markdownLexicalLine keeps syntax visibility separate from original content.
|
|
20
|
+
// Heading preserves inline source on its opening line and masks continuations;
|
|
21
|
+
// Prose retains a non-whitespace sentinel for inline examples so a tag after
|
|
22
|
+
// one cannot become line-leading metadata. Neither view changes source offsets.
|
|
23
|
+
type markdownLexicalLine struct {
|
|
24
|
+
Heading string
|
|
25
|
+
Prose string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// markdownRegion records a half-open range in the original UTF-8 source.
|
|
29
|
+
type markdownRegion struct {
|
|
30
|
+
Start int
|
|
31
|
+
End int
|
|
32
|
+
Block bool
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// markdownHybridParser adds the existing HTML/MDX example carriers to a
|
|
36
|
+
// CommonMark parse. It consumes the original reader instead of projecting or
|
|
37
|
+
// deleting bytes, so paragraph and container boundaries remain authoritative.
|
|
38
|
+
type markdownHybridParser struct {
|
|
39
|
+
Source string
|
|
40
|
+
Protected []markdownRegion
|
|
41
|
+
Owners []markdownRegion
|
|
42
|
+
Ends map[ast.Node]int
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// markdownHybridBlock is deliberately not an ast.Paragraph. Goldmark gives
|
|
46
|
+
// paragraphs its own continuation policy; this node owns example continuations
|
|
47
|
+
// while retaining original source segments for the standard inline parser.
|
|
48
|
+
var markdownHybridBlockKind = ast.NewNodeKind("EvidenceMarkdownExample")
|
|
49
|
+
|
|
50
|
+
type markdownHybridBlock struct{ ast.BaseBlock }
|
|
51
|
+
|
|
52
|
+
func (block *markdownHybridBlock) Kind() ast.NodeKind { return markdownHybridBlockKind }
|
|
53
|
+
func (block *markdownHybridBlock) Dump(_ []byte) *ast.NodeDump { return ast.NewNodeDump(block, nil) }
|
|
54
|
+
|
|
55
|
+
// Free block parsers run after every registered trigger parser regardless of
|
|
56
|
+
// priority. Register every byte so an existing owner can precede fence/list/
|
|
57
|
+
// quote openers, and a new carrier can precede the ordinary HTML block parser.
|
|
58
|
+
var markdownHybridTriggers = func() []byte {
|
|
59
|
+
triggers := make([]byte, 256)
|
|
60
|
+
for index := range triggers {
|
|
61
|
+
triggers[index] = byte(index)
|
|
62
|
+
}
|
|
63
|
+
return triggers
|
|
64
|
+
}()
|
|
65
|
+
|
|
66
|
+
func (scan *markdownHybridParser) Trigger() []byte { return markdownHybridTriggers }
|
|
67
|
+
|
|
68
|
+
func (scan *markdownHybridParser) ownerAt(position int) (markdownRegion, bool) {
|
|
69
|
+
index := sort.Search(len(scan.Owners), func(index int) bool {
|
|
70
|
+
return scan.Owners[index].End > position
|
|
71
|
+
})
|
|
72
|
+
if index < len(scan.Owners) && scan.Owners[index].Start <= position {
|
|
73
|
+
return scan.Owners[index], true
|
|
74
|
+
}
|
|
75
|
+
return markdownRegion{}, false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func (scan *markdownHybridParser) protectedAt(position int) (markdownRegion, bool) {
|
|
79
|
+
index := sort.Search(len(scan.Protected), func(index int) bool {
|
|
80
|
+
return scan.Protected[index].End > position
|
|
81
|
+
})
|
|
82
|
+
if index < len(scan.Protected) && scan.Protected[index].Start <= position {
|
|
83
|
+
region := scan.Protected[index]
|
|
84
|
+
if _, owned := scan.ownerAt(region.Start); !owned {
|
|
85
|
+
return region, true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return markdownRegion{}, false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// findOwners records only lexical carriers outside earlier code and comments.
|
|
92
|
+
// A standard-parser code region beginning inside an already acquired hybrid
|
|
93
|
+
// carrier has no authority: the hybrid carrier owns those delimiter bytes.
|
|
94
|
+
func (scan *markdownHybridParser) findOwners(start int, end int) int {
|
|
95
|
+
ownerEnd := start
|
|
96
|
+
for cursor := start; cursor < end; {
|
|
97
|
+
if owner, found := scan.ownerAt(cursor); found {
|
|
98
|
+
ownerEnd = max(ownerEnd, owner.End)
|
|
99
|
+
cursor = owner.End
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if region, found := scan.protectedAt(cursor); found {
|
|
103
|
+
cursor = region.End
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if scan.Source[cursor] == '\\' && cursor+1 < len(scan.Source) {
|
|
107
|
+
cursor += 2
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
next := markdownOpaqueEnd(scan.Source, cursor)
|
|
111
|
+
if next > cursor {
|
|
112
|
+
scan.Owners = append(scan.Owners, markdownRegion{Start: cursor, End: next})
|
|
113
|
+
ownerEnd = max(ownerEnd, next)
|
|
114
|
+
cursor = next
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
cursor++
|
|
118
|
+
}
|
|
119
|
+
return ownerEnd
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func (scan *markdownHybridParser) Open(_ ast.Node, reader text.Reader, _ parser.Context) (ast.Node, parser.State) {
|
|
123
|
+
_, segment := reader.PeekLine()
|
|
124
|
+
end := scan.findOwners(segment.Start, segment.Stop)
|
|
125
|
+
if end <= segment.Stop {
|
|
126
|
+
return nil, parser.NoChildren
|
|
127
|
+
}
|
|
128
|
+
node := &markdownHybridBlock{}
|
|
129
|
+
node.Init(node)
|
|
130
|
+
node.AppendSource(segment)
|
|
131
|
+
scan.Ends[node] = end
|
|
132
|
+
reader.AdvanceToEOL()
|
|
133
|
+
return node, parser.NoChildren
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
func (scan *markdownHybridParser) Continue(node ast.Node, reader text.Reader, _ parser.Context) parser.State {
|
|
137
|
+
line, segment := reader.PeekLine()
|
|
138
|
+
if util.IsBlank(line) || segment.Start >= scan.Ends[node] {
|
|
139
|
+
return parser.Close
|
|
140
|
+
}
|
|
141
|
+
scan.Ends[node] = max(scan.Ends[node], scan.findOwners(segment.Start, segment.Stop))
|
|
142
|
+
node.(ast.BlockNode).AppendSource(segment)
|
|
143
|
+
reader.AdvanceToEOL()
|
|
144
|
+
return parser.Continue | parser.NoChildren
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func (scan *markdownHybridParser) Close(node ast.Node, _ text.Reader, _ parser.Context) {
|
|
148
|
+
delete(scan.Ends, node)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func (scan *markdownHybridParser) CanInterruptParagraph() bool { return true }
|
|
152
|
+
func (scan *markdownHybridParser) CanAcceptIndentedLine() bool { return false }
|
|
153
|
+
|
|
154
|
+
// markdownHybridCarry reacquires a known owner after a paragraph/container
|
|
155
|
+
// boundary, before the bytes inside it can open an unrelated Markdown block.
|
|
156
|
+
// It never acquires new owners and therefore cannot steal ordinary code blocks.
|
|
157
|
+
type markdownHybridCarry struct{ Scan *markdownHybridParser }
|
|
158
|
+
|
|
159
|
+
func (carry *markdownHybridCarry) Trigger() []byte { return markdownHybridTriggers }
|
|
160
|
+
func (carry *markdownHybridCarry) Open(_ ast.Node, reader text.Reader, _ parser.Context) (ast.Node, parser.State) {
|
|
161
|
+
line, segment := reader.PeekLine()
|
|
162
|
+
owner, found := carry.Scan.ownerAt(segment.Start)
|
|
163
|
+
if !found || owner.Start >= segment.Start || util.IsBlank(line) {
|
|
164
|
+
return nil, parser.NoChildren
|
|
165
|
+
}
|
|
166
|
+
node := &markdownHybridBlock{}
|
|
167
|
+
node.Init(node)
|
|
168
|
+
node.AppendSource(segment)
|
|
169
|
+
carry.Scan.Ends[node] = max(owner.End, carry.Scan.findOwners(segment.Start, segment.Stop))
|
|
170
|
+
reader.AdvanceToEOL()
|
|
171
|
+
return node, parser.NoChildren
|
|
172
|
+
}
|
|
173
|
+
func (carry *markdownHybridCarry) Continue(node ast.Node, reader text.Reader, context parser.Context) parser.State {
|
|
174
|
+
return carry.Scan.Continue(node, reader, context)
|
|
175
|
+
}
|
|
176
|
+
func (carry *markdownHybridCarry) Close(node ast.Node, reader text.Reader, context parser.Context) {
|
|
177
|
+
carry.Scan.Close(node, reader, context)
|
|
178
|
+
}
|
|
179
|
+
func (carry *markdownHybridCarry) CanInterruptParagraph() bool { return true }
|
|
180
|
+
func (carry *markdownHybridCarry) CanAcceptIndentedLine() bool { return true }
|
|
181
|
+
|
|
182
|
+
type markdownHybridInline struct{ Scan *markdownHybridParser }
|
|
183
|
+
|
|
184
|
+
func (inline *markdownHybridInline) Trigger() []byte { return []byte{' ', '<', '=', '`'} }
|
|
185
|
+
|
|
186
|
+
func (inline *markdownHybridInline) Parse(parent ast.Node, reader text.Reader, _ parser.Context) ast.Node {
|
|
187
|
+
_, segment := reader.Position()
|
|
188
|
+
owner, found := inline.Scan.ownerAt(segment.Start)
|
|
189
|
+
if !found {
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
source := parent.(ast.BlockNode).Source()
|
|
193
|
+
end := min(owner.End, source[len(source)-1].Stop)
|
|
194
|
+
start := segment.Start
|
|
195
|
+
for {
|
|
196
|
+
line, segment := reader.PeekLine()
|
|
197
|
+
if line == nil {
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
if end <= segment.Stop {
|
|
201
|
+
reader.Advance(end - segment.Start)
|
|
202
|
+
break
|
|
203
|
+
}
|
|
204
|
+
reader.AdvanceLine()
|
|
205
|
+
}
|
|
206
|
+
return ast.NewRawHTML(text.NewMultiLineValueFromIndex(text.NewIndex(start, end), text.IdentityDecoder))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// markdownParserRegions reads code positions from the maintained CommonMark
|
|
210
|
+
// parser. Containers and multiline spans use its original source positions.
|
|
211
|
+
func markdownParserRegions(source string, document ast.Node, comments bool) []markdownRegion {
|
|
212
|
+
regions := []markdownRegion{}
|
|
213
|
+
ast.Walk(document, func(node ast.Node, entering bool) (ast.WalkStatus, error) {
|
|
214
|
+
if !entering {
|
|
215
|
+
return ast.WalkContinue, nil
|
|
216
|
+
}
|
|
217
|
+
switch value := node.(type) {
|
|
218
|
+
case *ast.CodeBlock:
|
|
219
|
+
start, end := value.Pos(), value.Pos()
|
|
220
|
+
for _, segment := range value.Value.Segments() {
|
|
221
|
+
end = segment.Stop
|
|
222
|
+
}
|
|
223
|
+
start = strings.LastIndexByte(source[:start], '\n') + 1
|
|
224
|
+
regions = append(regions, markdownRegion{Start: start, End: end, Block: true})
|
|
225
|
+
case *ast.CodeSpan:
|
|
226
|
+
indices := value.Value.Indices()
|
|
227
|
+
if len(indices) != 0 {
|
|
228
|
+
start, end := value.Pos(), indices[len(indices)-1].Stop
|
|
229
|
+
for end < len(source) && source[end] == '`' {
|
|
230
|
+
end++
|
|
231
|
+
}
|
|
232
|
+
regions = append(regions, markdownRegion{Start: start, End: end})
|
|
233
|
+
}
|
|
234
|
+
case *ast.HTMLBlock:
|
|
235
|
+
if comments && value.HTMLBlockKind == ast.HTMLBlockKind2 {
|
|
236
|
+
end := markdownCommentEnd(source, value.Pos())
|
|
237
|
+
regions = append(regions, markdownRegion{Start: value.Pos(), End: end})
|
|
238
|
+
}
|
|
239
|
+
case *ast.RawHTML:
|
|
240
|
+
if comments && strings.HasPrefix(source[value.Pos():], "<!--") {
|
|
241
|
+
indices := value.Value.Indices()
|
|
242
|
+
if len(indices) != 0 {
|
|
243
|
+
regions = append(regions, markdownRegion{Start: value.Pos(), End: indices[len(indices)-1].Stop})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return ast.WalkContinue, nil
|
|
248
|
+
})
|
|
249
|
+
sort.Slice(regions, func(left, right int) bool { return regions[left].Start < regions[right].Start })
|
|
250
|
+
return regions
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// scanMarkdownRegions gives every annotation consumer the same original-byte
|
|
254
|
+
// classification. Two parser passes preserve CommonMark ownership before adding
|
|
255
|
+
// the existing permissive MDX carriers through public parser extension APIs.
|
|
256
|
+
// No rendered text or normalized projection participates in source offsets.
|
|
257
|
+
func scanMarkdownRegions(content string, lines []string) ([]markdownLexicalLine, [][2]int) {
|
|
258
|
+
baseline := parser.New().Parse([]byte(content))
|
|
259
|
+
scan := &markdownHybridParser{Source: content, Protected: markdownParserRegions(content, baseline, true), Ends: map[ast.Node]int{}}
|
|
260
|
+
// All block entries share one receiver's ownership state. A known owner
|
|
261
|
+
// resumes before new blocks; headings acquire carriers before ATX parsing,
|
|
262
|
+
// and ordinary carriers acquire ownership after code and quote openers.
|
|
263
|
+
document := parser.New(parser.WithBlockParsers(
|
|
264
|
+
util.Prioritized[parser.BlockParser](&markdownHybridCarry{Scan: scan}, 0),
|
|
265
|
+
util.Prioritized[parser.BlockParser](&markdownHybridHeading{Scan: scan}, 590),
|
|
266
|
+
util.Prioritized[parser.BlockParser](scan, 850),
|
|
267
|
+
), parser.WithInlineParsers(util.Prioritized[parser.InlineParser](&markdownHybridInline{Scan: scan}, 50))).Parse([]byte(content))
|
|
268
|
+
regions := markdownParserRegions(content, document, false)
|
|
269
|
+
code := make([]byte, len(content))
|
|
270
|
+
for _, region := range regions {
|
|
271
|
+
kind := byte(1)
|
|
272
|
+
if region.Block {
|
|
273
|
+
kind = 2
|
|
274
|
+
}
|
|
275
|
+
for cursor := region.Start; cursor < region.End; cursor++ {
|
|
276
|
+
code[cursor] = kind
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
lexical := make([]markdownLexicalLine, len(lines))
|
|
280
|
+
spans := [][2]int{}
|
|
281
|
+
commentStart := -1
|
|
282
|
+
opaqueStart, opaqueEnd := 0, 0
|
|
283
|
+
offset := 0
|
|
284
|
+
for index, rawLine := range lines {
|
|
285
|
+
line := strings.TrimSuffix(rawLine, "\r")
|
|
286
|
+
heading, prose := []byte(line), []byte(line)
|
|
287
|
+
mask := func(start, end int, structural bool) {
|
|
288
|
+
for cursor := start; cursor < end; cursor++ {
|
|
289
|
+
prose[cursor] = ' '
|
|
290
|
+
if structural {
|
|
291
|
+
heading[cursor] = ' '
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for cursor := 0; cursor < len(line); {
|
|
296
|
+
if commentStart >= 0 {
|
|
297
|
+
end, closed := len(line), false
|
|
298
|
+
if offset+cursor == commentStart+4 && strings.HasPrefix(line[cursor:], ">") {
|
|
299
|
+
end, closed = cursor+1, true
|
|
300
|
+
} else if offset+cursor == commentStart+4 && strings.HasPrefix(line[cursor:], "->") {
|
|
301
|
+
end, closed = cursor+2, true
|
|
302
|
+
} else if closing := strings.Index(line[cursor:], "-->"); closing >= 0 {
|
|
303
|
+
end, closed = cursor+closing+3, true
|
|
304
|
+
}
|
|
305
|
+
mask(cursor, end, true)
|
|
306
|
+
if commentStart < offset {
|
|
307
|
+
heading[cursor] = 'x'
|
|
308
|
+
}
|
|
309
|
+
cursor = end
|
|
310
|
+
if closed {
|
|
311
|
+
spans = append(spans, [2]int{commentStart, offset + end})
|
|
312
|
+
commentStart = -1
|
|
313
|
+
}
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
if opaqueEnd > offset+cursor {
|
|
317
|
+
end := min(len(line), opaqueEnd-offset)
|
|
318
|
+
continuation := opaqueStart < offset
|
|
319
|
+
mask(cursor, end, continuation)
|
|
320
|
+
if continuation {
|
|
321
|
+
heading[cursor] = 'x'
|
|
322
|
+
}
|
|
323
|
+
prose[cursor] = 'x'
|
|
324
|
+
cursor = end
|
|
325
|
+
continue
|
|
326
|
+
}
|
|
327
|
+
if kind := code[offset+cursor]; kind != 0 {
|
|
328
|
+
end := cursor + 1
|
|
329
|
+
for end < len(line) && code[offset+end] == kind {
|
|
330
|
+
end++
|
|
331
|
+
}
|
|
332
|
+
mask(cursor, end, kind == 2)
|
|
333
|
+
prose[cursor] = 'x'
|
|
334
|
+
cursor = end
|
|
335
|
+
continue
|
|
336
|
+
}
|
|
337
|
+
if line[cursor] == '\\' && cursor+1 < len(line) && strings.ContainsRune("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", rune(line[cursor+1])) {
|
|
338
|
+
mask(cursor, cursor+2, false)
|
|
339
|
+
prose[cursor] = 'x'
|
|
340
|
+
cursor += 2
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
if strings.HasPrefix(line[cursor:], "<!--") {
|
|
344
|
+
commentStart = offset + cursor
|
|
345
|
+
mask(cursor, cursor+4, true)
|
|
346
|
+
cursor += 4
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
if end := markdownOpaqueEnd(content, offset+cursor); end > offset+cursor {
|
|
350
|
+
opaqueStart, opaqueEnd = offset+cursor, end
|
|
351
|
+
continue
|
|
352
|
+
}
|
|
353
|
+
cursor++
|
|
354
|
+
}
|
|
355
|
+
lexical[index] = markdownLexicalLine{Heading: string(heading), Prose: string(prose)}
|
|
356
|
+
offset += len(rawLine) + 1
|
|
357
|
+
}
|
|
358
|
+
return lexical, spans
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
type markdownHybridHeading struct {
|
|
362
|
+
Scan *markdownHybridParser
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
func (heading *markdownHybridHeading) Trigger() []byte { return []byte{'#'} }
|
|
366
|
+
func (heading *markdownHybridHeading) Open(parent ast.Node, reader text.Reader, context parser.Context) (ast.Node, parser.State) {
|
|
367
|
+
line, segment := reader.PeekLine()
|
|
368
|
+
if _, _, found := markdownHeading(string(line)); !found {
|
|
369
|
+
return nil, parser.NoChildren
|
|
370
|
+
}
|
|
371
|
+
node, state := heading.Scan.Open(parent, reader, context)
|
|
372
|
+
if node != nil {
|
|
373
|
+
// An ATX heading owns exactly one source line. Its rendered carrier may
|
|
374
|
+
// continue, but backticks in the heading cannot pair with the next block.
|
|
375
|
+
heading.Scan.Ends[node] = segment.Stop
|
|
376
|
+
}
|
|
377
|
+
return node, state
|
|
378
|
+
}
|
|
379
|
+
func (heading *markdownHybridHeading) Continue(node ast.Node, reader text.Reader, context parser.Context) parser.State {
|
|
380
|
+
return heading.Scan.Continue(node, reader, context)
|
|
381
|
+
}
|
|
382
|
+
func (heading *markdownHybridHeading) Close(node ast.Node, reader text.Reader, context parser.Context) {
|
|
383
|
+
heading.Scan.Close(node, reader, context)
|
|
384
|
+
}
|
|
385
|
+
func (heading *markdownHybridHeading) CanInterruptParagraph() bool { return true }
|
|
386
|
+
func (heading *markdownHybridHeading) CanAcceptIndentedLine() bool { return false }
|
|
387
|
+
|
|
388
|
+
// markdownOpaqueEnd consumes supported HTML syntax and template attributes with
|
|
389
|
+
// quote-aware original-byte boundaries. An unclosed rendered owner retains its
|
|
390
|
+
// suffix, matching the existing permissive Markdown/MDX documentation contract.
|
|
391
|
+
func markdownOpaqueEnd(content string, start int) int {
|
|
392
|
+
if strings.HasPrefix(content[start:], "={`") {
|
|
393
|
+
return markdownTemplateEnd(content, start+3)
|
|
394
|
+
}
|
|
395
|
+
if content[start] != '<' || start+1 >= len(content) {
|
|
396
|
+
return start
|
|
397
|
+
}
|
|
398
|
+
cursor := start + 1
|
|
399
|
+
closing := content[cursor] == '/'
|
|
400
|
+
if closing {
|
|
401
|
+
cursor++
|
|
402
|
+
}
|
|
403
|
+
name := cursor
|
|
404
|
+
for cursor < len(content) && (content[cursor] >= 'a' && content[cursor] <= 'z' || content[cursor] >= 'A' && content[cursor] <= 'Z' || cursor > name && (content[cursor] >= '0' && content[cursor] <= '9' || content[cursor] == '-' || content[cursor] == ':')) {
|
|
405
|
+
cursor++
|
|
406
|
+
}
|
|
407
|
+
if cursor == name {
|
|
408
|
+
return start
|
|
409
|
+
}
|
|
410
|
+
if cursor < len(content) && !strings.ContainsRune(" \t\r\n/>", rune(content[cursor])) {
|
|
411
|
+
return start
|
|
412
|
+
}
|
|
413
|
+
pre := !closing && strings.EqualFold(content[name:cursor], "pre")
|
|
414
|
+
quote := byte(0)
|
|
415
|
+
for cursor < len(content) {
|
|
416
|
+
char := content[cursor]
|
|
417
|
+
if quote != 0 {
|
|
418
|
+
if char == quote {
|
|
419
|
+
quote = 0
|
|
420
|
+
}
|
|
421
|
+
cursor++
|
|
422
|
+
} else if char == '\'' || char == '"' {
|
|
423
|
+
quote = char
|
|
424
|
+
cursor++
|
|
425
|
+
} else if strings.HasPrefix(content[cursor:], "={`") {
|
|
426
|
+
cursor = markdownTemplateEnd(content, cursor+3)
|
|
427
|
+
} else if char == '>' {
|
|
428
|
+
cursor++
|
|
429
|
+
if pre {
|
|
430
|
+
for probe := cursor; probe+6 <= len(content); probe++ {
|
|
431
|
+
if strings.EqualFold(content[probe:probe+6], "</pre>") {
|
|
432
|
+
return probe + 6
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return len(content)
|
|
436
|
+
}
|
|
437
|
+
return cursor
|
|
438
|
+
} else if char == '<' {
|
|
439
|
+
return start
|
|
440
|
+
} else {
|
|
441
|
+
cursor++
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if pre {
|
|
445
|
+
return len(content)
|
|
446
|
+
}
|
|
447
|
+
return start
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// markdownCommentEnd clips a protected comment to its actual closing byte;
|
|
451
|
+
// CommonMark HTML blocks may retain unrelated text from the closing line.
|
|
452
|
+
func markdownCommentEnd(content string, start int) int {
|
|
453
|
+
body := start + 4
|
|
454
|
+
if strings.HasPrefix(content[body:], ">") {
|
|
455
|
+
return body + 1
|
|
456
|
+
}
|
|
457
|
+
if strings.HasPrefix(content[body:], "->") {
|
|
458
|
+
return body + 2
|
|
459
|
+
}
|
|
460
|
+
if close := strings.Index(content[body:], "-->"); close >= 0 {
|
|
461
|
+
return body + close + 3
|
|
462
|
+
}
|
|
463
|
+
return len(content)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
func markdownTemplateEnd(content string, from int) int {
|
|
467
|
+
for cursor := from; cursor < len(content); cursor++ {
|
|
468
|
+
if content[cursor] == '\\' && cursor+1 < len(content) {
|
|
469
|
+
cursor++
|
|
470
|
+
} else if strings.HasPrefix(content[cursor:], "`}") {
|
|
471
|
+
return cursor + 2
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return len(content)
|
|
475
|
+
}
|
|
476
|
+
|
|
15
477
|
// loadMarkdownInventories reads every configured Markdown population, once per
|
|
16
478
|
// distinct base.
|
|
17
479
|
//
|
|
@@ -23,13 +485,18 @@ var explicitAnchorPattern = regexp.MustCompile(`\s*\{#([A-Za-z0-9][A-Za-z0-9._:-
|
|
|
23
485
|
func loadMarkdownInventories(
|
|
24
486
|
root string,
|
|
25
487
|
config graphConfig,
|
|
488
|
+
captures ...*markdownCapture,
|
|
26
489
|
) (map[string]*artifactInventory, graphDiagnostics) {
|
|
490
|
+
capture := &markdownCapture{}
|
|
491
|
+
if len(captures) != 0 && captures[0] != nil {
|
|
492
|
+
capture = captures[0]
|
|
493
|
+
}
|
|
27
494
|
inventories := map[string]*artifactInventory{}
|
|
28
495
|
problems := graphDiagnostics{}
|
|
29
496
|
for _, base := range configuredBases(config, artifactMarkdown) {
|
|
30
497
|
problems = append(
|
|
31
498
|
problems,
|
|
32
|
-
loadMarkdownBase(base, config, inventories)...,
|
|
499
|
+
loadMarkdownBase(base, config, inventories, capture)...,
|
|
33
500
|
)
|
|
34
501
|
}
|
|
35
502
|
return inventories, problems
|
|
@@ -39,19 +506,35 @@ func loadMarkdownBase(
|
|
|
39
506
|
base populationBase,
|
|
40
507
|
config graphConfig,
|
|
41
508
|
inventories map[string]*artifactInventory,
|
|
509
|
+
capture *markdownCapture,
|
|
42
510
|
) graphDiagnostics {
|
|
43
511
|
problems := graphDiagnostics{}
|
|
44
512
|
severity := populationSeverity(config, artifactMarkdown, base, "", "*", false)
|
|
45
|
-
|
|
513
|
+
from, problem := capture.resolve(base)
|
|
514
|
+
if problem != "" {
|
|
46
515
|
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
47
516
|
return problems.add(severity, problem)
|
|
48
517
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
518
|
+
project := func(relative string, inventory *artifactInventory, readErr error) {
|
|
519
|
+
address := base.addressOf(relative)
|
|
520
|
+
inventories[address.Key] = inventory
|
|
521
|
+
severity := populationSeverity(config, artifactMarkdown, base, relative, "*", false)
|
|
522
|
+
if readErr != nil {
|
|
523
|
+
problems = problems.add(severity,
|
|
524
|
+
"Evidence graph could not read Markdown file '"+address.Display+"': "+causeText(readErr)+". Fix filesystem access or exclude the file from configured globs.",
|
|
525
|
+
)
|
|
526
|
+
return
|
|
527
|
+
}
|
|
528
|
+
for _, inventoryProblem := range inventory.Problems {
|
|
529
|
+
if selectedByMarkdownPopulation(config, base, relative, inventoryProblem.Symbol) {
|
|
530
|
+
problems = problems.add(populationSeverity(config, artifactMarkdown, base, relative, inventoryProblem.Symbol, false), inventoryProblem.Message)
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Unreadable tags concern the selected file, independently of its symbols
|
|
534
|
+
// or health. They retain the same phase-specific file severity.
|
|
535
|
+
problems = problems.add(severity, inventory.Unreadable...)
|
|
53
536
|
}
|
|
54
|
-
err :=
|
|
537
|
+
err := capture.walk(base, from, config, func(current string, entry fs.DirEntry, walkErr error) error {
|
|
55
538
|
if walkErr != nil {
|
|
56
539
|
// The walk root belongs to its population by construction, so a failure
|
|
57
540
|
// to list it is a failure of the population and is never decided by what
|
|
@@ -107,35 +590,20 @@ func loadMarkdownBase(
|
|
|
107
590
|
if !matchesConfiguredMarkdownFile(config, base, relative) {
|
|
108
591
|
return nil
|
|
109
592
|
}
|
|
110
|
-
severity := populationSeverity(config, artifactMarkdown, base, relative, "*", false)
|
|
111
593
|
address := base.addressOf(relative)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
inventories[address.Key] = &artifactInventory{
|
|
115
|
-
Path: address.Display,
|
|
116
|
-
Type: artifactMarkdown,
|
|
117
|
-
LoadFailed: true,
|
|
118
|
-
}
|
|
119
|
-
problems = problems.add(
|
|
120
|
-
severity,
|
|
121
|
-
"Evidence graph could not read Markdown file '"+address.Display+"': "+causeText(readErr)+". Fix filesystem access or exclude the file from configured globs.",
|
|
122
|
-
)
|
|
123
|
-
return nil
|
|
124
|
-
}
|
|
125
|
-
inventory, _ := scanMarkdownInventory(address, string(content))
|
|
126
|
-
inventories[address.Key] = inventory
|
|
127
|
-
for _, inventoryProblem := range inventory.Problems {
|
|
128
|
-
if selectedByMarkdownPopulation(config, base, relative, inventoryProblem.Symbol) {
|
|
129
|
-
problems = problems.add(populationSeverity(config, artifactMarkdown, base, relative, inventoryProblem.Symbol, false), inventoryProblem.Message)
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
// An unreadable tag is not a health question and not a symbol question
|
|
133
|
-
// either: the file loaded, its units are complete, and the tag reaches no
|
|
134
|
-
// host whichever symbol a reference selects. The walk already refuses a
|
|
135
|
-
// path no configured glob takes, so reaching here is enough to report.
|
|
136
|
-
problems = problems.add(severity, inventory.Unreadable...)
|
|
594
|
+
inventory, readErr := capture.read(address, current)
|
|
595
|
+
project(relative, inventory, readErr)
|
|
137
596
|
return nil
|
|
138
597
|
})
|
|
598
|
+
// An expanded reference selection may need fresh discovery. Files already
|
|
599
|
+
// consumed remain part of this Check's snapshot even if that later walk no
|
|
600
|
+
// longer sees them. Reproject their original inventory and failures only
|
|
601
|
+
// where this phase selects the same address.
|
|
602
|
+
for relative, captured := range capture.inventories[base.Absolute] {
|
|
603
|
+
if _, seen := inventories[base.address(relative)]; !seen && matchesConfiguredMarkdownFile(config, base, relative) {
|
|
604
|
+
project(relative, captured.inventory, captured.err)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
139
607
|
if err != nil {
|
|
140
608
|
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
141
609
|
problems = problems.add(severity, unlistableBaseProblem(base, "Markdown", err))
|
|
@@ -180,14 +648,14 @@ func scanMarkdownInventory(
|
|
|
180
648
|
})
|
|
181
649
|
}
|
|
182
650
|
|
|
651
|
+
// A byte order mark is encoding, not content. A document that opens with one
|
|
652
|
+
// renders its first heading, but the mark would sit in front of the `#` and
|
|
653
|
+
// hide that heading from the scan, so its unit and every obligation it owes
|
|
654
|
+
// would vanish without a word.
|
|
655
|
+
content = strings.TrimPrefix(content, "\xef\xbb\xbf")
|
|
183
656
|
lines := strings.Split(content, "\n")
|
|
184
657
|
hostAtLine := make([]string, len(lines))
|
|
185
658
|
hostIDAtLine := make([]string, len(lines))
|
|
186
|
-
fencedAtLine := make([]bool, len(lines))
|
|
187
|
-
// commentAtLine marks the lines a citation or a review can live on, so the
|
|
188
|
-
// content digest can leave them out. A fenced block is not marked: an
|
|
189
|
-
// `<!-- -->` inside one hosts no tag, and its text is content of the section.
|
|
190
|
-
commentAtLine := make([]bool, len(lines))
|
|
191
659
|
// The nearest heading *unit* enclosing each line, which is not the same as its
|
|
192
660
|
// host: a heading may open a region without materializing a unit. Kept apart
|
|
193
661
|
// from hostIDAtLine because that value decides where a declaration sits, and
|
|
@@ -196,62 +664,11 @@ func scanMarkdownInventory(
|
|
|
196
664
|
currentDigestHostID := fileUnitID
|
|
197
665
|
currentHost := "file"
|
|
198
666
|
currentHostID := fileUnitID
|
|
199
|
-
|
|
200
|
-
fenceLength := 0
|
|
201
|
-
inHTMLComment := false
|
|
667
|
+
lexical, commentSpans := scanMarkdownRegions(content, lines)
|
|
202
668
|
headingUnitIDs := [5]string{}
|
|
203
|
-
for index
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
if marker, length, remainder, ok := markdownFence(line); ok {
|
|
207
|
-
fencedAtLine[index] = true
|
|
208
|
-
hostIDAtLine[index] = currentHostID
|
|
209
|
-
// Fenced content is content. It hosts no tag, so it is never excluded as a
|
|
210
|
-
// tag position, and leaving it unattributed would drop every code block out
|
|
211
|
-
// of its section's digest: rewriting the example in a cited section would
|
|
212
|
-
// then expire nothing.
|
|
213
|
-
digestHostIDAtLine[index] = currentDigestHostID
|
|
214
|
-
if fenceMarker == 0 {
|
|
215
|
-
fenceMarker = marker
|
|
216
|
-
fenceLength = length
|
|
217
|
-
} else if marker == fenceMarker &&
|
|
218
|
-
length >= fenceLength &&
|
|
219
|
-
strings.TrimSpace(remainder) == "" {
|
|
220
|
-
fenceMarker = 0
|
|
221
|
-
fenceLength = 0
|
|
222
|
-
}
|
|
223
|
-
hostAtLine[index] = currentHost
|
|
224
|
-
continue
|
|
225
|
-
}
|
|
226
|
-
if fenceMarker != 0 {
|
|
227
|
-
fencedAtLine[index] = true
|
|
228
|
-
hostAtLine[index] = currentHost
|
|
229
|
-
hostIDAtLine[index] = currentHostID
|
|
230
|
-
digestHostIDAtLine[index] = currentDigestHostID
|
|
231
|
-
continue
|
|
232
|
-
}
|
|
233
|
-
if inHTMLComment {
|
|
234
|
-
if strings.Contains(trimmed, "-->") {
|
|
235
|
-
inHTMLComment = false
|
|
236
|
-
}
|
|
237
|
-
hostAtLine[index] = currentHost
|
|
238
|
-
hostIDAtLine[index] = currentHostID
|
|
239
|
-
digestHostIDAtLine[index] = currentDigestHostID
|
|
240
|
-
commentAtLine[index] = true
|
|
241
|
-
continue
|
|
242
|
-
}
|
|
243
|
-
if strings.HasPrefix(trimmed, "<!--") {
|
|
244
|
-
remainder := strings.TrimPrefix(trimmed, "<!--")
|
|
245
|
-
if !strings.Contains(remainder, "-->") {
|
|
246
|
-
inHTMLComment = true
|
|
247
|
-
}
|
|
248
|
-
hostAtLine[index] = currentHost
|
|
249
|
-
hostIDAtLine[index] = currentHostID
|
|
250
|
-
digestHostIDAtLine[index] = currentDigestHostID
|
|
251
|
-
commentAtLine[index] = true
|
|
252
|
-
continue
|
|
253
|
-
}
|
|
254
|
-
level, title, ok := markdownHeading(line)
|
|
669
|
+
for index := range lines {
|
|
670
|
+
visible := lexical[index].Heading
|
|
671
|
+
level, title, ok := markdownHeading(visible)
|
|
255
672
|
if ok {
|
|
256
673
|
currentHost = "h" + decimal(level)
|
|
257
674
|
currentHostID = "markdown:" + address.Key + ":" + currentHost + ":" + decimal(index+1)
|
|
@@ -333,19 +750,22 @@ func scanMarkdownInventory(
|
|
|
333
750
|
digestHostIDAtLine[index] = currentDigestHostID
|
|
334
751
|
}
|
|
335
752
|
|
|
336
|
-
reportUnreadableMarkdownTags(inventory, address.Display,
|
|
753
|
+
reportUnreadableMarkdownTags(inventory, address.Display, lexical)
|
|
337
754
|
|
|
338
755
|
sequence := 0
|
|
339
|
-
for _, match := range
|
|
340
|
-
if len(match) < 4 {
|
|
341
|
-
continue
|
|
342
|
-
}
|
|
756
|
+
for _, match := range commentSpans {
|
|
343
757
|
commentStart := match[0]
|
|
344
758
|
line := lineAt(content, commentStart)
|
|
345
|
-
if line <= 0 || line > len(lines)
|
|
759
|
+
if line <= 0 || line > len(lines) {
|
|
760
|
+
continue
|
|
761
|
+
}
|
|
762
|
+
// `<!-->` and `<!--->` are complete comments with no body, so their closing
|
|
763
|
+
// marker overlaps their opening one and there is nothing to parse.
|
|
764
|
+
bodyStart, bodyEnd := match[0]+4, match[1]-3
|
|
765
|
+
if bodyEnd < bodyStart {
|
|
346
766
|
continue
|
|
347
767
|
}
|
|
348
|
-
comment := content[
|
|
768
|
+
comment := content[bodyStart:bodyEnd]
|
|
349
769
|
for _, parsed := range parseDeclarations(comment) {
|
|
350
770
|
sequence++
|
|
351
771
|
inventory.Declarations = append(inventory.Declarations, &evidenceDeclaration{
|
|
@@ -375,7 +795,7 @@ func scanMarkdownInventory(
|
|
|
375
795
|
})
|
|
376
796
|
}
|
|
377
797
|
}
|
|
378
|
-
assignMarkdownDigests(inventory, lines, digestHostIDAtLine,
|
|
798
|
+
assignMarkdownDigests(inventory, content, lines, digestHostIDAtLine, commentSpans)
|
|
379
799
|
return inventory, problems
|
|
380
800
|
}
|
|
381
801
|
|
|
@@ -395,34 +815,72 @@ func scanMarkdownInventory(
|
|
|
395
815
|
// `evidenceUnit.Digest` records the consequence; do not carry the Markdown
|
|
396
816
|
// intuition across.
|
|
397
817
|
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
818
|
+
// The text cut out of every digest is exactly what the declaration scan reads
|
|
819
|
+
// as a tag position: each closed HTML comment outside code and rendered
|
|
820
|
+
// examples. The shared lexical scan records exact original-byte spans, so a
|
|
821
|
+
// span may open after prose, close before prose, or run
|
|
822
|
+
// across lines, and it may not be a whole line. Cutting spans rather than lines
|
|
823
|
+
// keeps the prose beside a comment in the digest, so a content change there
|
|
824
|
+
// still expires a review, while writing the review changes nothing it is
|
|
825
|
+
// checked against. A `<!--` that never closes matches no span, is read as no
|
|
826
|
+
// tag, and so stays content.
|
|
827
|
+
//
|
|
828
|
+
// A line left holding nothing but comment spans is dropped, and so is the one
|
|
829
|
+
// blank line after it when a blank line stood before it. A comment written as a
|
|
830
|
+
// paragraph of its own is set off by blank lines on both sides, and keeping
|
|
831
|
+
// both would let adding or removing the comment add or remove a blank line of
|
|
832
|
+
// the unit it sits in, which is the tag position changing the digest after all.
|
|
401
833
|
func assignMarkdownDigests(
|
|
402
834
|
inventory *artifactInventory,
|
|
835
|
+
content string,
|
|
403
836
|
lines []string,
|
|
404
837
|
digestHostIDAtLine []string,
|
|
405
|
-
|
|
838
|
+
spans [][2]int,
|
|
406
839
|
) {
|
|
407
840
|
owned := map[string][]string{}
|
|
408
|
-
|
|
841
|
+
next := 0
|
|
842
|
+
lineStart := 0
|
|
843
|
+
// Whether the last line kept was blank, and whether the blank line that
|
|
844
|
+
// follows a dropped comment paragraph is the second of its pair.
|
|
845
|
+
keptBlank := false
|
|
846
|
+
dropBlank := false
|
|
847
|
+
for index, rawLine := range lines {
|
|
848
|
+
lineEnd := lineStart + len(rawLine)
|
|
409
849
|
id := digestHostIDAtLine[index]
|
|
410
|
-
|
|
411
|
-
|
|
850
|
+
for next < len(spans) && spans[next][1] <= lineStart {
|
|
851
|
+
next++
|
|
412
852
|
}
|
|
853
|
+
remainder := strings.Builder{}
|
|
854
|
+
cursor := lineStart
|
|
855
|
+
cut := false
|
|
856
|
+
for k := next; k < len(spans) && spans[k][0] < lineEnd; k++ {
|
|
857
|
+
cut = true
|
|
858
|
+
if spans[k][0] > cursor {
|
|
859
|
+
remainder.WriteString(content[cursor:spans[k][0]])
|
|
860
|
+
}
|
|
861
|
+
cursor = max(cursor, min(spans[k][1], lineEnd))
|
|
862
|
+
}
|
|
863
|
+
if cursor < lineEnd {
|
|
864
|
+
remainder.WriteString(content[cursor:lineEnd])
|
|
865
|
+
}
|
|
866
|
+
lineStart = lineEnd + 1
|
|
867
|
+
text := strings.TrimSuffix(remainder.String(), "\r")
|
|
413
868
|
if id == "" {
|
|
414
869
|
continue
|
|
415
870
|
}
|
|
416
|
-
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
871
|
+
blank := strings.TrimSpace(text) == ""
|
|
872
|
+
// A line holding nothing but comment spans is a tag position, not content.
|
|
873
|
+
if cut && blank {
|
|
874
|
+
dropBlank = keptBlank
|
|
875
|
+
continue
|
|
876
|
+
}
|
|
877
|
+
if blank && dropBlank {
|
|
878
|
+
dropBlank = false
|
|
879
|
+
continue
|
|
880
|
+
}
|
|
881
|
+
dropBlank = false
|
|
882
|
+
keptBlank = blank
|
|
883
|
+
owned[id] = append(owned[id], text)
|
|
426
884
|
}
|
|
427
885
|
for _, unit := range inventory.Units {
|
|
428
886
|
unit.Digest = contentDigest(strings.Join(owned[unit.ID], "\n"))
|
|
@@ -612,8 +1070,10 @@ func markdownSlug(title string) string {
|
|
|
612
1070
|
// A fenced block is an example rather than a citation and stays silent, which
|
|
613
1071
|
// is not a concession: this product's own documentation shows tags inside
|
|
614
1072
|
// fences, and reporting them would fail its build. An indented code block is
|
|
615
|
-
// the same case in another spelling
|
|
616
|
-
//
|
|
1073
|
+
// the same case in another spelling. The maintained CommonMark parser decides
|
|
1074
|
+
// indentation relative to the enclosing list and quote containers. Inline code,
|
|
1075
|
+
// escaped delimiters and the supported HTML/MDX example carriers stay silent
|
|
1076
|
+
// under the same lexical classification used by headings and metadata.
|
|
617
1077
|
//
|
|
618
1078
|
// The tag has to open its line, which is the discrimination every reader in
|
|
619
1079
|
// this package performs, so a sentence mentioning one describes it rather than
|
|
@@ -621,45 +1081,19 @@ func markdownSlug(title string) string {
|
|
|
621
1081
|
func reportUnreadableMarkdownTags(
|
|
622
1082
|
inventory *artifactInventory,
|
|
623
1083
|
location string,
|
|
624
|
-
|
|
625
|
-
fencedAtLine []bool,
|
|
626
|
-
commentAtLine []bool,
|
|
1084
|
+
lexical []markdownLexicalLine,
|
|
627
1085
|
) {
|
|
628
1086
|
if inventory == nil {
|
|
629
1087
|
return
|
|
630
1088
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
line := strings.TrimSuffix(rawLine, "\r")
|
|
634
|
-
if opens, closes := renderedCodeEdges(line); opens || closes {
|
|
635
|
-
rendered = opens
|
|
636
|
-
continue
|
|
637
|
-
}
|
|
638
|
-
if rendered {
|
|
639
|
-
continue
|
|
640
|
-
}
|
|
641
|
-
if index < len(fencedAtLine) && fencedAtLine[index] {
|
|
642
|
-
continue
|
|
643
|
-
}
|
|
644
|
-
if index < len(commentAtLine) && commentAtLine[index] {
|
|
645
|
-
continue
|
|
646
|
-
}
|
|
647
|
-
if strings.HasPrefix(line, " ") || strings.HasPrefix(line, " ") {
|
|
648
|
-
continue
|
|
649
|
-
}
|
|
650
|
-
trimmed := markdownLineContent(line)
|
|
1089
|
+
for index, line := range lexical {
|
|
1090
|
+
trimmed := markdownLineContent(line.Prose)
|
|
651
1091
|
if tag, _, found := declarationLine(trimmed); found {
|
|
652
|
-
inventory.Unreadable = append(
|
|
653
|
-
inventory.Unreadable,
|
|
654
|
-
unreadableMarkdownProblem("@"+string(tag), location, index+1),
|
|
655
|
-
)
|
|
1092
|
+
inventory.Unreadable = append(inventory.Unreadable, unreadableMarkdownProblem("@"+string(tag), location, index+1))
|
|
656
1093
|
continue
|
|
657
1094
|
}
|
|
658
1095
|
if reviews, _, opened := reviewLine(trimmed); opened {
|
|
659
|
-
inventory.Unreadable = append(
|
|
660
|
-
inventory.Unreadable,
|
|
661
|
-
unreadableMarkdownProblem(reviewMarkerFor(reviews), location, index+1),
|
|
662
|
-
)
|
|
1096
|
+
inventory.Unreadable = append(inventory.Unreadable, unreadableMarkdownProblem(reviewMarkerFor(reviews), location, index+1))
|
|
663
1097
|
}
|
|
664
1098
|
}
|
|
665
1099
|
}
|
|
@@ -711,30 +1145,6 @@ func markdownListMarker(content string) int {
|
|
|
711
1145
|
return digits + 2
|
|
712
1146
|
}
|
|
713
1147
|
|
|
714
|
-
// renderedCodeEdges reports whether a line opens or closes a block that renders
|
|
715
|
-
// as code without being a fence.
|
|
716
|
-
//
|
|
717
|
-
// A documentation site shows examples through more than one syntax. An MDX page
|
|
718
|
-
// passes a template literal to a component, and an HTML page uses `<pre>`; both
|
|
719
|
-
// render as code, so both are examples in the sense a fence is, and the repair
|
|
720
|
-
// this diagnostic names would delete the example from the rendered page rather
|
|
721
|
-
// than fix anything. Only the two edges are recognized, because a page that
|
|
722
|
-
// opens one and never closes it is a page whose own build fails first.
|
|
723
|
-
func renderedCodeEdges(line string) (bool, bool) {
|
|
724
|
-
lowered := strings.ToLower(line)
|
|
725
|
-
switch {
|
|
726
|
-
case strings.Contains(lowered, "<pre"):
|
|
727
|
-
return !strings.Contains(lowered, "</pre>"), strings.Contains(lowered, "</pre>")
|
|
728
|
-
case strings.Contains(lowered, "</pre>"):
|
|
729
|
-
return false, true
|
|
730
|
-
case strings.Contains(line, "={`"):
|
|
731
|
-
return !strings.Contains(line, "`}"), strings.Contains(line, "`}")
|
|
732
|
-
case strings.Contains(line, "`}"):
|
|
733
|
-
return false, true
|
|
734
|
-
}
|
|
735
|
-
return false, false
|
|
736
|
-
}
|
|
737
|
-
|
|
738
1148
|
// unreadableMarkdownProblem names the position and the move that fixes it.
|
|
739
1149
|
func unreadableMarkdownProblem(tag string, location string, line int) string {
|
|
740
1150
|
return "Unreadable " + tag + " at " + location + ":" + decimal(line) +
|