@ttsc/evidence 0.25.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/LICENSE +21 -0
- package/README.md +279 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +87 -0
- package/lib/internal/loadPrismaModels.d.ts +1 -0
- package/lib/internal/loadPrismaModels.js +197 -0
- package/lib/internal/loadSwaggerOperations.d.ts +1 -0
- package/lib/internal/loadSwaggerOperations.js +156 -0
- package/lib/internal/normalizeSwaggerDocument.d.ts +1 -0
- package/lib/internal/normalizeSwaggerDocument.js +7 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.d.ts +32 -0
- package/lib/structures/ITtscEvidenceDocumentedConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaim.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.d.ts +102 -0
- package/lib/structures/ITtscEvidenceGraphClaimBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphConfig.d.ts +19 -0
- package/lib/structures/ITtscEvidenceGraphConfig.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.d.ts +40 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.d.ts +76 -0
- package/lib/structures/ITtscEvidenceGraphMarkdownReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.d.ts +65 -0
- package/lib/structures/ITtscEvidenceGraphPrismaClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.d.ts +75 -0
- package/lib/structures/ITtscEvidenceGraphPrismaReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReference.d.ts +14 -0
- package/lib/structures/ITtscEvidenceGraphReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.d.ts +73 -0
- package/lib/structures/ITtscEvidenceGraphReferenceBase.js +2 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.d.ts +39 -0
- package/lib/structures/ITtscEvidenceGraphSwaggerReference.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.d.ts +54 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptClaim.js +2 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.d.ts +83 -0
- package/lib/structures/ITtscEvidenceGraphTypeScriptReference.js +2 -0
- package/lib/structures/index.d.ts +13 -0
- package/lib/structures/index.js +29 -0
- package/lib/typings/ITtscLintContributorRules.d.ts +34 -0
- package/lib/typings/ITtscLintContributorRules.js +2 -0
- package/lib/typings/ITtscLintRuleOptionsMap.d.ts +20 -0
- package/lib/typings/ITtscLintRuleOptionsMap.js +2 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.d.ts +32 -0
- package/lib/typings/TtscEvidenceGraphMarkdownSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.d.ts +38 -0
- package/lib/typings/TtscEvidenceGraphPrismaSymbol.js +2 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.d.ts +64 -0
- package/lib/typings/TtscEvidenceGraphTypeScriptSymbol.js +2 -0
- package/lib/typings/index.d.ts +5 -0
- package/lib/typings/index.js +21 -0
- package/native/base.go +298 -0
- package/native/config.go +798 -0
- package/native/declaration.go +240 -0
- package/native/documented.go +353 -0
- package/native/exports.go +484 -0
- package/native/glob.go +207 -0
- package/native/graph.go +1409 -0
- package/native/health.go +45 -0
- package/native/hints.go +261 -0
- package/native/imports.go +373 -0
- package/native/inputs.go +125 -0
- package/native/loader.go +424 -0
- package/native/markdown.go +423 -0
- package/native/model.go +296 -0
- package/native/prisma.go +719 -0
- package/native/prisma_cache.go +213 -0
- package/native/prisma_locator.go +417 -0
- package/native/singular.go +369 -0
- package/native/swagger.go +331 -0
- package/native/swagger_cache.go +274 -0
- package/native/todo.go +138 -0
- package/native/typescript.go +1199 -0
- package/native/typescript_cache.go +96 -0
- package/package.json +64 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
package evidence
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"io/fs"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
7
|
+
"regexp"
|
|
8
|
+
"strings"
|
|
9
|
+
"unicode"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
var markdownCommentPattern = regexp.MustCompile(`(?s)<!--(.*?)-->`)
|
|
13
|
+
var explicitAnchorPattern = regexp.MustCompile(`\s*\{#([A-Za-z0-9][A-Za-z0-9._:-]*)\}\s*$`)
|
|
14
|
+
|
|
15
|
+
// loadMarkdownInventories reads every configured Markdown population, once per
|
|
16
|
+
// distinct base.
|
|
17
|
+
//
|
|
18
|
+
// One walk per base rather than one walk for the project, because a population
|
|
19
|
+
// that declares a root sits outside the tree the project walk covers — and a
|
|
20
|
+
// walk that started high enough to cover both would descend through everything
|
|
21
|
+
// between them. Two populations sharing a base share one walk, so the cost
|
|
22
|
+
// tracks the roots an author declared rather than the populations they wrote.
|
|
23
|
+
func loadMarkdownInventories(
|
|
24
|
+
root string,
|
|
25
|
+
config graphConfig,
|
|
26
|
+
) (map[string]*artifactInventory, []string) {
|
|
27
|
+
inventories := map[string]*artifactInventory{}
|
|
28
|
+
problems := []string{}
|
|
29
|
+
for _, base := range configuredBases(config, artifactMarkdown) {
|
|
30
|
+
problems = append(
|
|
31
|
+
problems,
|
|
32
|
+
loadMarkdownBase(base, config, inventories)...,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
return inventories, problems
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func loadMarkdownBase(
|
|
39
|
+
base populationBase,
|
|
40
|
+
config graphConfig,
|
|
41
|
+
inventories map[string]*artifactInventory,
|
|
42
|
+
) []string {
|
|
43
|
+
problems := []string{}
|
|
44
|
+
if problem := unreadableBaseProblem(base, artifactMarkdown); problem != "" {
|
|
45
|
+
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
46
|
+
return []string{problem}
|
|
47
|
+
}
|
|
48
|
+
err := filepath.WalkDir(base.Absolute, func(current string, entry fs.DirEntry, walkErr error) error {
|
|
49
|
+
if walkErr != nil {
|
|
50
|
+
relative, ok := relativeProjectPath(base.Absolute, current)
|
|
51
|
+
relevant := ok &&
|
|
52
|
+
(matchesConfiguredMarkdownFile(config, base, relative) ||
|
|
53
|
+
couldContainConfiguredMarkdown(config, base, relative))
|
|
54
|
+
if !relevant {
|
|
55
|
+
if entry != nil && entry.IsDir() {
|
|
56
|
+
return filepath.SkipDir
|
|
57
|
+
}
|
|
58
|
+
return nil
|
|
59
|
+
}
|
|
60
|
+
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
61
|
+
problems = append(problems, "Evidence graph could not inspect '"+current+"': "+walkErr.Error()+". Fix filesystem access so configured Markdown sources can be indexed.")
|
|
62
|
+
if entry != nil && entry.IsDir() {
|
|
63
|
+
return filepath.SkipDir
|
|
64
|
+
}
|
|
65
|
+
return nil
|
|
66
|
+
}
|
|
67
|
+
if entry.IsDir() {
|
|
68
|
+
if current != base.Absolute {
|
|
69
|
+
relative, ok := relativeProjectPath(base.Absolute, current)
|
|
70
|
+
if !ok || !couldContainConfiguredMarkdown(config, base, relative) {
|
|
71
|
+
return filepath.SkipDir
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return nil
|
|
75
|
+
}
|
|
76
|
+
relative, ok := relativeProjectPath(base.Absolute, current)
|
|
77
|
+
if !ok {
|
|
78
|
+
return nil
|
|
79
|
+
}
|
|
80
|
+
if !matchesConfiguredMarkdownFile(config, base, relative) {
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
address := base.addressOf(relative)
|
|
84
|
+
content, readErr := os.ReadFile(current)
|
|
85
|
+
if readErr != nil {
|
|
86
|
+
inventories[address.Key] = &artifactInventory{
|
|
87
|
+
Path: address.Display,
|
|
88
|
+
Type: artifactMarkdown,
|
|
89
|
+
LoadFailed: true,
|
|
90
|
+
}
|
|
91
|
+
problems = append(problems, "Evidence graph could not read Markdown file '"+address.Display+"': "+readErr.Error()+". Fix filesystem access or exclude the file from configured globs.")
|
|
92
|
+
return nil
|
|
93
|
+
}
|
|
94
|
+
inventory, _ := scanMarkdownInventory(address, string(content))
|
|
95
|
+
inventories[address.Key] = inventory
|
|
96
|
+
for _, inventoryProblem := range inventory.Problems {
|
|
97
|
+
if selectedByMarkdownReference(config, base, relative, inventoryProblem.Symbol) {
|
|
98
|
+
problems = append(problems, inventoryProblem.Message)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return nil
|
|
102
|
+
})
|
|
103
|
+
if err != nil {
|
|
104
|
+
recordPopulationFailure(inventories, artifactMarkdown, base)
|
|
105
|
+
problems = append(problems, "Evidence graph could not walk Markdown root '"+populationRootLabel(base)+"': "+err.Error()+".")
|
|
106
|
+
}
|
|
107
|
+
return problems
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func scanMarkdownInventory(
|
|
111
|
+
address artifactAddress,
|
|
112
|
+
content string,
|
|
113
|
+
) (*artifactInventory, []string) {
|
|
114
|
+
// The target is the path inside the population's base, while the location is
|
|
115
|
+
// the path a reader opens. They are the same string for a project-rooted
|
|
116
|
+
// population and deliberately differ for a rooted one: a citation that keeps
|
|
117
|
+
// working when the document set is adopted by a sibling package cannot carry
|
|
118
|
+
// that package's distance from the documents.
|
|
119
|
+
path := address.Relative
|
|
120
|
+
inventory := &artifactInventory{
|
|
121
|
+
Path: address.Display,
|
|
122
|
+
Type: artifactMarkdown,
|
|
123
|
+
}
|
|
124
|
+
problems := []string{}
|
|
125
|
+
targetablePath := !containsWhitespace(path)
|
|
126
|
+
fileUnitID := ""
|
|
127
|
+
if targetablePath {
|
|
128
|
+
fileUnitID = "markdown:" + address.Key + ":file"
|
|
129
|
+
inventory.Units = append(inventory.Units, &evidenceUnit{
|
|
130
|
+
ID: fileUnitID,
|
|
131
|
+
Target: path,
|
|
132
|
+
Type: artifactMarkdown,
|
|
133
|
+
Symbol: "file",
|
|
134
|
+
Path: address.Display,
|
|
135
|
+
Line: 1,
|
|
136
|
+
Readable: "Markdown file",
|
|
137
|
+
})
|
|
138
|
+
} else {
|
|
139
|
+
problem := "Markdown file '" + address.Display + "' cannot form an evidence target because its path contains whitespace. Rename the file so '@evidence <target> <reason>' can represent its path as one target token."
|
|
140
|
+
problems = append(problems, problem)
|
|
141
|
+
inventory.Problems = append(inventory.Problems, inventoryProblem{
|
|
142
|
+
Symbol: "*",
|
|
143
|
+
Message: problem,
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines := strings.Split(content, "\n")
|
|
148
|
+
hostAtLine := make([]string, len(lines))
|
|
149
|
+
hostIDAtLine := make([]string, len(lines))
|
|
150
|
+
fencedAtLine := make([]bool, len(lines))
|
|
151
|
+
currentHost := "file"
|
|
152
|
+
currentHostID := fileUnitID
|
|
153
|
+
fenceMarker := rune(0)
|
|
154
|
+
fenceLength := 0
|
|
155
|
+
inHTMLComment := false
|
|
156
|
+
headingUnitIDs := [5]string{}
|
|
157
|
+
for index, rawLine := range lines {
|
|
158
|
+
line := strings.TrimSuffix(rawLine, "\r")
|
|
159
|
+
trimmed := strings.TrimLeft(line, " \t")
|
|
160
|
+
if marker, length, remainder, ok := markdownFence(line); ok {
|
|
161
|
+
fencedAtLine[index] = true
|
|
162
|
+
hostIDAtLine[index] = currentHostID
|
|
163
|
+
if fenceMarker == 0 {
|
|
164
|
+
fenceMarker = marker
|
|
165
|
+
fenceLength = length
|
|
166
|
+
} else if marker == fenceMarker &&
|
|
167
|
+
length >= fenceLength &&
|
|
168
|
+
strings.TrimSpace(remainder) == "" {
|
|
169
|
+
fenceMarker = 0
|
|
170
|
+
fenceLength = 0
|
|
171
|
+
}
|
|
172
|
+
hostAtLine[index] = currentHost
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
if fenceMarker != 0 {
|
|
176
|
+
fencedAtLine[index] = true
|
|
177
|
+
hostAtLine[index] = currentHost
|
|
178
|
+
hostIDAtLine[index] = currentHostID
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
if inHTMLComment {
|
|
182
|
+
if strings.Contains(trimmed, "-->") {
|
|
183
|
+
inHTMLComment = false
|
|
184
|
+
}
|
|
185
|
+
hostAtLine[index] = currentHost
|
|
186
|
+
hostIDAtLine[index] = currentHostID
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
if strings.HasPrefix(trimmed, "<!--") {
|
|
190
|
+
remainder := strings.TrimPrefix(trimmed, "<!--")
|
|
191
|
+
if !strings.Contains(remainder, "-->") {
|
|
192
|
+
inHTMLComment = true
|
|
193
|
+
}
|
|
194
|
+
hostAtLine[index] = currentHost
|
|
195
|
+
hostIDAtLine[index] = currentHostID
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
level, title, ok := markdownHeading(line)
|
|
199
|
+
if ok {
|
|
200
|
+
currentHost = "h" + decimal(level)
|
|
201
|
+
currentHostID = "markdown:" + address.Key + ":" + currentHost + ":" + decimal(index+1)
|
|
202
|
+
if level <= 4 {
|
|
203
|
+
for descendantLevel := level; descendantLevel <= 4; descendantLevel++ {
|
|
204
|
+
headingUnitIDs[descendantLevel] = ""
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if level <= 4 && targetablePath {
|
|
208
|
+
title, anchor := markdownHeadingIdentity(title)
|
|
209
|
+
if anchor == "" {
|
|
210
|
+
problems = append(
|
|
211
|
+
problems,
|
|
212
|
+
"Markdown evidence unit at "+address.Display+":"+decimal(index+1)+" has no resolvable anchor. Add a non-empty heading title or an explicit '{#anchor}' suffix.",
|
|
213
|
+
)
|
|
214
|
+
inventory.Problems = append(inventory.Problems, inventoryProblem{
|
|
215
|
+
Symbol: currentHost,
|
|
216
|
+
Message: problems[len(problems)-1],
|
|
217
|
+
})
|
|
218
|
+
} else {
|
|
219
|
+
parentID := fileUnitID
|
|
220
|
+
for ancestorLevel := level - 1; ancestorLevel >= 1; ancestorLevel-- {
|
|
221
|
+
if headingUnitIDs[ancestorLevel] != "" {
|
|
222
|
+
parentID = headingUnitIDs[ancestorLevel]
|
|
223
|
+
break
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
unit := &evidenceUnit{
|
|
227
|
+
ID: "markdown:" + address.Key + ":" + currentHost + ":" + decimal(index+1),
|
|
228
|
+
ParentID: parentID,
|
|
229
|
+
Target: path + "#" + anchor,
|
|
230
|
+
Type: artifactMarkdown,
|
|
231
|
+
Symbol: currentHost,
|
|
232
|
+
Path: address.Display,
|
|
233
|
+
Line: index + 1,
|
|
234
|
+
Readable: "Markdown " + strings.ToUpper(currentHost) + " '" + title + "'",
|
|
235
|
+
}
|
|
236
|
+
inventory.Units = append(inventory.Units, unit)
|
|
237
|
+
headingUnitIDs[level] = unit.ID
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
hostAtLine[index] = currentHost
|
|
242
|
+
hostIDAtLine[index] = currentHostID
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
sequence := 0
|
|
246
|
+
for _, match := range markdownCommentPattern.FindAllStringSubmatchIndex(content, -1) {
|
|
247
|
+
if len(match) < 4 {
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
commentStart := match[0]
|
|
251
|
+
line := lineAt(content, commentStart)
|
|
252
|
+
if line <= 0 || line > len(lines) || fencedAtLine[line-1] {
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
comment := content[match[2]:match[3]]
|
|
256
|
+
for _, parsed := range parseDeclarations(comment) {
|
|
257
|
+
sequence++
|
|
258
|
+
inventory.Declarations = append(inventory.Declarations, &evidenceDeclaration{
|
|
259
|
+
ID: "markdown:" + address.Key + ":" + decimal(line+parsed.LineOffset) + ":" + decimal(sequence),
|
|
260
|
+
HostID: hostIDAtLine[line-1],
|
|
261
|
+
SemanticHostIDs: []string{hostIDAtLine[line-1]},
|
|
262
|
+
Type: artifactMarkdown,
|
|
263
|
+
Tag: parsed.Tag,
|
|
264
|
+
Target: parsed.Target,
|
|
265
|
+
Reason: parsed.Reason,
|
|
266
|
+
Hosts: symbolSet{hostAtLine[line-1]: true},
|
|
267
|
+
Path: address.Display,
|
|
268
|
+
Line: line + parsed.LineOffset,
|
|
269
|
+
Sequence: sequence,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return inventory, problems
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// matchesConfiguredMarkdownFile reports whether a population rooted at this base
|
|
277
|
+
// selects the file.
|
|
278
|
+
//
|
|
279
|
+
// The base is compared before the globs, because a walk covers one base at a
|
|
280
|
+
// time and another base's patterns say nothing about a path inside this one.
|
|
281
|
+
// Without that comparison a project-rooted `docs/**` would sweep in the
|
|
282
|
+
// `docs` directory of every declared root.
|
|
283
|
+
func matchesConfiguredMarkdownFile(
|
|
284
|
+
config graphConfig,
|
|
285
|
+
base populationBase,
|
|
286
|
+
path string,
|
|
287
|
+
) bool {
|
|
288
|
+
for _, claim := range config.Claims {
|
|
289
|
+
if claim.Type == artifactMarkdown &&
|
|
290
|
+
claim.Base.Absolute == base.Absolute &&
|
|
291
|
+
claim.Files.matches(path) {
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
for _, reference := range claim.References {
|
|
295
|
+
if reference.Type == artifactMarkdown &&
|
|
296
|
+
reference.Base.Absolute == base.Absolute &&
|
|
297
|
+
reference.Files.matches(path) {
|
|
298
|
+
return true
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
func couldContainConfiguredMarkdown(
|
|
306
|
+
config graphConfig,
|
|
307
|
+
base populationBase,
|
|
308
|
+
directory string,
|
|
309
|
+
) bool {
|
|
310
|
+
for _, claim := range config.Claims {
|
|
311
|
+
if claim.Type == artifactMarkdown &&
|
|
312
|
+
claim.Base.Absolute == base.Absolute &&
|
|
313
|
+
claim.Files.couldMatchDescendant(directory) {
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
for _, reference := range claim.References {
|
|
317
|
+
if reference.Type == artifactMarkdown &&
|
|
318
|
+
reference.Base.Absolute == base.Absolute &&
|
|
319
|
+
reference.Files.couldMatchDescendant(directory) {
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
func selectedByMarkdownReference(
|
|
328
|
+
config graphConfig,
|
|
329
|
+
base populationBase,
|
|
330
|
+
path string,
|
|
331
|
+
symbol string,
|
|
332
|
+
) bool {
|
|
333
|
+
for _, claim := range config.Claims {
|
|
334
|
+
for _, reference := range claim.References {
|
|
335
|
+
if reference.Type == artifactMarkdown &&
|
|
336
|
+
reference.Base.Absolute == base.Absolute &&
|
|
337
|
+
reference.Files.matches(path) &&
|
|
338
|
+
(symbol == "*" || reference.Symbols.contains(symbol)) {
|
|
339
|
+
return true
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
func markdownFence(line string) (rune, int, string, bool) {
|
|
347
|
+
indent := 0
|
|
348
|
+
for indent < len(line) && line[indent] == ' ' {
|
|
349
|
+
indent++
|
|
350
|
+
}
|
|
351
|
+
if indent > 3 {
|
|
352
|
+
return 0, 0, "", false
|
|
353
|
+
}
|
|
354
|
+
runes := []rune(line[indent:])
|
|
355
|
+
if len(runes) < 3 || (runes[0] != '`' && runes[0] != '~') {
|
|
356
|
+
return 0, 0, "", false
|
|
357
|
+
}
|
|
358
|
+
count := 1
|
|
359
|
+
for count < len(runes) && runes[count] == runes[0] {
|
|
360
|
+
count++
|
|
361
|
+
}
|
|
362
|
+
if count < 3 {
|
|
363
|
+
return 0, 0, "", false
|
|
364
|
+
}
|
|
365
|
+
remainder := string(runes[count:])
|
|
366
|
+
if runes[0] == '`' && strings.Contains(remainder, "`") {
|
|
367
|
+
return 0, 0, "", false
|
|
368
|
+
}
|
|
369
|
+
return runes[0], count, remainder, true
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
func markdownHeading(line string) (int, string, bool) {
|
|
373
|
+
space := 0
|
|
374
|
+
for space < len(line) && line[space] == ' ' && space < 4 {
|
|
375
|
+
space++
|
|
376
|
+
}
|
|
377
|
+
if space > 3 || space >= len(line) || line[space] != '#' {
|
|
378
|
+
return 0, "", false
|
|
379
|
+
}
|
|
380
|
+
level := 0
|
|
381
|
+
for space+level < len(line) && line[space+level] == '#' {
|
|
382
|
+
level++
|
|
383
|
+
}
|
|
384
|
+
if level == 0 || level > 6 {
|
|
385
|
+
return 0, "", false
|
|
386
|
+
}
|
|
387
|
+
next := space + level
|
|
388
|
+
if next < len(line) && line[next] != ' ' && line[next] != '\t' {
|
|
389
|
+
return 0, "", false
|
|
390
|
+
}
|
|
391
|
+
title := strings.TrimSpace(line[next:])
|
|
392
|
+
trimmedHashes := strings.TrimRight(title, "#")
|
|
393
|
+
if trimmedHashes != title && (trimmedHashes == "" || strings.HasSuffix(trimmedHashes, " ") || strings.HasSuffix(trimmedHashes, "\t")) {
|
|
394
|
+
title = strings.TrimSpace(trimmedHashes)
|
|
395
|
+
}
|
|
396
|
+
return level, title, true
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
func markdownHeadingIdentity(title string) (string, string) {
|
|
400
|
+
if match := explicitAnchorPattern.FindStringSubmatch(title); len(match) == 2 {
|
|
401
|
+
cleanTitle := strings.TrimSpace(explicitAnchorPattern.ReplaceAllString(title, ""))
|
|
402
|
+
return cleanTitle, match[1]
|
|
403
|
+
}
|
|
404
|
+
return title, markdownSlug(title)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
func markdownSlug(title string) string {
|
|
408
|
+
var builder strings.Builder
|
|
409
|
+
lastHyphen := false
|
|
410
|
+
for _, char := range strings.ToLower(title) {
|
|
411
|
+
switch {
|
|
412
|
+
case unicode.IsLetter(char), unicode.IsNumber(char), char == '_':
|
|
413
|
+
builder.WriteRune(char)
|
|
414
|
+
lastHyphen = false
|
|
415
|
+
case char == '-' || unicode.IsSpace(char):
|
|
416
|
+
if builder.Len() > 0 && !lastHyphen {
|
|
417
|
+
builder.WriteRune('-')
|
|
418
|
+
lastHyphen = true
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return strings.Trim(builder.String(), "-")
|
|
423
|
+
}
|