Skip to content

Commit de857f7

Browse files
authored
Add table of contents scope management and refactor diagnostics (#774)
* Add table of contents scope management and refactor diagnostics Introduced `ITableOfContentsScope` to manage scope boundaries, improving link validation and table of contents processing. Updated diagnostics to emit hints for images outside the ToC scope and refactored functions for better reusability. Adjusted related tests and internal structures accordingly. * Add TODO to make this an error * post merge build fixes
1 parent f4c005e commit de857f7

File tree

12 files changed

+94
-63
lines changed

12 files changed

+94
-63
lines changed

docs/development/index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ navigation_title: Development
44

55
# Development Guide
66

7-
TODO write development documentation here
7+
TODO write development documentation here
8+
9+
10+
![Image outside of scope](../images/great-drawing-of-new-structure.png)

src/Elastic.Markdown/Diagnostics/ProcessorDiagnosticExtensions.cs

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ public static void EmitWarning(this IBlockExtension block, string message)
132132
block.Build.Collector.Channel.Write(d);
133133
}
134134

135-
136-
public static void EmitError(this InlineProcessor processor, LinkInline inline, string message)
135+
private static void LinkDiagnostic(InlineProcessor processor, Severity severity, LinkInline inline, string message)
137136
{
138137
var url = inline.Url;
139138
var line = inline.Line + 1;
@@ -145,36 +144,22 @@ public static void EmitError(this InlineProcessor processor, LinkInline inline,
145144
return;
146145
var d = new Diagnostic
147146
{
148-
Severity = Severity.Error,
147+
Severity = severity,
149148
File = processor.GetContext().MarkdownSourcePath.FullName,
150-
Column = column,
149+
Column = Math.Max(column, 1),
151150
Line = line,
152151
Message = message,
153152
Length = length
154153
};
155154
context.Build.Collector.Channel.Write(d);
156155
}
157156

157+
public static void EmitError(this InlineProcessor processor, LinkInline inline, string message) =>
158+
LinkDiagnostic(processor, Severity.Error, inline, message);
158159

159-
public static void EmitWarning(this InlineProcessor processor, LinkInline inline, string message)
160-
{
161-
var url = inline.Url;
162-
var line = inline.Line + 1;
163-
var column = inline.Column;
164-
var length = url?.Length ?? 1;
160+
public static void EmitWarning(this InlineProcessor processor, LinkInline inline, string message) =>
161+
LinkDiagnostic(processor, Severity.Warning, inline, message);
165162

166-
var context = processor.GetContext();
167-
if (context.SkipValidation)
168-
return;
169-
var d = new Diagnostic
170-
{
171-
Severity = Severity.Warning,
172-
File = processor.GetContext().MarkdownSourcePath.FullName,
173-
Column = column,
174-
Line = line,
175-
Message = message,
176-
Length = length
177-
};
178-
context.Build.Collector.Channel.Write(d);
179-
}
163+
public static void EmitHint(this InlineProcessor processor, LinkInline inline, string message) =>
164+
LinkDiagnostic(processor, Severity.Hint, inline, message);
180165
}

src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesDocsBuilderExtension.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,11 @@ HashSet<string> files
9797
if (f.Extension == ".toml")
9898
{
9999
var rule = DetectionRule.From(f);
100-
return new RuleReference(relativePath, detectionRules, true, [], rule);
100+
return new RuleReference(Build.Configuration, relativePath, detectionRules, true, [], rule);
101101
}
102102

103103
_ = files.Add(relativePath);
104-
return new FileReference(relativePath, true, false, []);
104+
return new FileReference(Build.Configuration, relativePath, true, false, []);
105105
})
106106
.OrderBy(d => d is RuleReference r ? r.Rule.Name : null, StringComparer.OrdinalIgnoreCase)
107107
.ToArray();

src/Elastic.Markdown/Extensions/DetectionRules/DetectionRulesReference.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66

77
namespace Elastic.Markdown.Extensions.DetectionRules;
88

9-
public record RulesFolderReference(string Path, bool Found, IReadOnlyCollection<ITocItem> Children) : ITocItem;
9+
public record RulesFolderReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, IReadOnlyCollection<ITocItem> Children) : ITocItem;
1010

11-
public record RuleReference(string Path, string SourceDirectory, bool Found, IReadOnlyCollection<ITocItem> Children, DetectionRule Rule)
12-
: FileReference(Path, Found, false, Children);
11+
public record RuleReference(
12+
ITableOfContentsScope TableOfContentsScope,
13+
string Path,
14+
string SourceDirectory,
15+
bool Found,
16+
IReadOnlyCollection<ITocItem> Children, DetectionRule Rule
17+
)
18+
: FileReference(TableOfContentsScope, Path, Found, false, Children);

src/Elastic.Markdown/IO/Configuration/ConfigurationFile.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.IO.Abstractions;
56
using DotNet.Globbing;
67
using Elastic.Markdown.Diagnostics;
78
using Elastic.Markdown.Extensions;
@@ -10,7 +11,7 @@
1011

1112
namespace Elastic.Markdown.IO.Configuration;
1213

13-
public record ConfigurationFile : DocumentationFile
14+
public record ConfigurationFile : DocumentationFile, ITableOfContentsScope
1415
{
1516
private readonly BuildContext _context;
1617

@@ -44,6 +45,8 @@ public record ConfigurationFile : DocumentationFile
4445
private FeatureFlags? _featureFlags;
4546
public FeatureFlags Features => _featureFlags ??= new FeatureFlags(_features);
4647

48+
public IDirectoryInfo ScopeDirectory { get; }
49+
4750
/// This is a documentation set that is not linked to by assembler.
4851
/// Setting this to true relaxes a few restrictions such as mixing toc references with file and folder reference
4952
public bool DevelopmentDocs { get; }
@@ -57,6 +60,7 @@ public ConfigurationFile(BuildContext context)
5760
: base(context.ConfigurationPath, context.DocumentationSourceDirectory)
5861
{
5962
_context = context;
63+
ScopeDirectory = context.ConfigurationPath.Directory!;
6064
if (!context.ConfigurationPath.Exists)
6165
{
6266
Project = "unknown";
@@ -122,7 +126,7 @@ public ConfigurationFile(BuildContext context)
122126
switch (entry.Key)
123127
{
124128
case "toc":
125-
var toc = new TableOfContentsConfiguration(this, _context, 0, "");
129+
var toc = new TableOfContentsConfiguration(this, ScopeDirectory, _context, 0, "");
126130
var children = toc.ReadChildren(reader, entry.Entry);
127131
var tocEntries = children.OfType<TocReference>().ToArray();
128132
if (!DevelopmentDocs && !IsNarrativeDocs && tocEntries.Length > 0 && children.Count != tocEntries.Length)

src/Elastic.Markdown/IO/Configuration/ITocItem.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44

55
namespace Elastic.Markdown.IO.Configuration;
66

7-
public interface ITocItem;
7+
public interface ITocItem
8+
{
9+
ITableOfContentsScope TableOfContentsScope { get; }
10+
}
811

9-
public record FileReference(string Path, bool Found, bool Hidden, IReadOnlyCollection<ITocItem> Children) : ITocItem;
12+
public record FileReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, bool Hidden, IReadOnlyCollection<ITocItem> Children)
13+
: ITocItem;
1014

11-
public record FolderReference(string Path, bool Found, bool InNav, IReadOnlyCollection<ITocItem> Children) : ITocItem;
15+
public record FolderReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, bool InNav, IReadOnlyCollection<ITocItem> Children)
16+
: ITocItem;
1217

13-
public record TocReference(string Path, bool Found, bool InNav, IReadOnlyCollection<ITocItem> Children) : FolderReference(Path, Found, InNav, Children);
18+
public record TocReference(ITableOfContentsScope TableOfContentsScope, string Path, bool Found, bool InNav, IReadOnlyCollection<ITocItem> Children)
19+
: FolderReference(TableOfContentsScope, Path, Found, InNav, Children);

src/Elastic.Markdown/IO/Configuration/TableOfContentsConfiguration.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,32 @@
99

1010
namespace Elastic.Markdown.IO.Configuration;
1111

12-
public record TableOfContentsConfiguration
12+
public interface ITableOfContentsScope
13+
{
14+
IDirectoryInfo ScopeDirectory { get; }
15+
}
16+
17+
public record TableOfContentsConfiguration : ITableOfContentsScope
1318
{
14-
private readonly ConfigurationFile _configuration;
1519
private readonly BuildContext _context;
20+
private readonly int _maxTocDepth;
1621
private readonly int _depth;
1722
private readonly string _parentPath;
1823
private readonly IDirectoryInfo _rootPath;
24+
private readonly ConfigurationFile _configuration;
1925

2026
public HashSet<string> Files { get; } = new(StringComparer.OrdinalIgnoreCase);
2127

2228
public IReadOnlyCollection<ITocItem> TableOfContents { get; private set; } = [];
2329

24-
public TableOfContentsConfiguration(ConfigurationFile configuration, BuildContext context, int depth, string parentPath)
30+
public IDirectoryInfo ScopeDirectory { get; }
31+
32+
public TableOfContentsConfiguration(ConfigurationFile configuration, IDirectoryInfo scope, BuildContext context, int depth, string parentPath)
2533
{
26-
_rootPath = context.DocumentationSourceDirectory;
2734
_configuration = configuration;
35+
ScopeDirectory = scope;
36+
_maxTocDepth = configuration.MaxTocDepth;
37+
_rootPath = context.DocumentationSourceDirectory;
2838
_context = context;
2939
_depth = depth;
3040
_parentPath = parentPath;
@@ -33,7 +43,7 @@ public TableOfContentsConfiguration(ConfigurationFile configuration, BuildContex
3343
public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyValuePair<YamlNode, YamlNode> entry, string? parentPath = null)
3444
{
3545
parentPath ??= _parentPath;
36-
if (_depth > _configuration.MaxTocDepth)
46+
if (_depth > _maxTocDepth)
3747
{
3848
reader.EmitError($"toc.yml files may only be linked from docset.yml", entry.Key);
3949
return [];
@@ -112,7 +122,7 @@ public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyVa
112122
foreach (var f in toc.Files)
113123
_ = Files.Add(f);
114124

115-
return [new TocReference($"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, inNav, toc.TableOfContents)];
125+
return [new TocReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, inNav, toc.TableOfContents)];
116126
}
117127

118128
if (file is not null)
@@ -133,15 +143,15 @@ public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyVa
133143
children = extension.CreateTableOfContentItems(parentPath, detectionRules, Files);
134144
}
135145
}
136-
return [new FileReference($"{parentPath}{Path.DirectorySeparatorChar}{file}".TrimStart(Path.DirectorySeparatorChar), fileFound, hiddenFile, children ?? [])];
146+
return [new FileReference(this, $"{parentPath}{Path.DirectorySeparatorChar}{file}".TrimStart(Path.DirectorySeparatorChar), fileFound, hiddenFile, children ?? [])];
137147
}
138148

139149
if (folder is not null)
140150
{
141151
if (children is null)
142152
_ = _configuration.ImplicitFolders.Add(parentPath.TrimStart(Path.DirectorySeparatorChar));
143153

144-
return [new FolderReference($"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, inNav, children ?? [])];
154+
return [new FolderReference(this, $"{parentPath}".TrimStart(Path.DirectorySeparatorChar), folderFound, inNav, children ?? [])];
145155
}
146156

147157
return null;
@@ -235,7 +245,7 @@ public IReadOnlyCollection<ITocItem> ReadChildren(YamlStreamReader reader, KeyVa
235245
switch (kv.Key)
236246
{
237247
case "toc":
238-
var nestedConfiguration = new TableOfContentsConfiguration(_configuration, _context, _depth + 1, fullTocPath);
248+
var nestedConfiguration = new TableOfContentsConfiguration(_configuration, source.Directory!, _context, _depth + 1, fullTocPath);
239249
_ = nestedConfiguration.ReadChildren(reader, kv.Entry);
240250
return nestedConfiguration;
241251
}

src/Elastic.Markdown/IO/MarkdownFile.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Runtime.InteropServices;
77
using Elastic.Markdown.Diagnostics;
88
using Elastic.Markdown.Helpers;
9+
using Elastic.Markdown.IO.Configuration;
910
using Elastic.Markdown.IO.Navigation;
1011
using Elastic.Markdown.Myst;
1112
using Elastic.Markdown.Myst.Directives;
@@ -45,6 +46,9 @@ DocumentationSet set
4546
_configurationFile = build.Configuration.SourceFile;
4647
_globalSubstitutions = build.Configuration.Substitutions;
4748
_set = set;
49+
//may be updated by DocumentationGroup.ProcessTocItems
50+
//todo refactor mutability of MarkdownFile as a whole
51+
ScopeDirectory = build.Configuration.ScopeDirectory;
4852
}
4953

5054
public string Id { get; } = Guid.NewGuid().ToString("N")[..8];
@@ -80,8 +84,8 @@ public string? NavigationTitle
8084
}
8185

8286
//indexed by slug
83-
private readonly Dictionary<string, PageTocItem> _tableOfContent = new(StringComparer.OrdinalIgnoreCase);
84-
public IReadOnlyDictionary<string, PageTocItem> TableOfContents => _tableOfContent;
87+
private readonly Dictionary<string, PageTocItem> _pageTableOfContent = new(StringComparer.OrdinalIgnoreCase);
88+
public IReadOnlyDictionary<string, PageTocItem> PageTableOfContent => _pageTableOfContent;
8589

8690
private readonly HashSet<string> _anchors = new(StringComparer.OrdinalIgnoreCase);
8791
public IReadOnlySet<string> Anchors => _anchors;
@@ -146,6 +150,8 @@ public string[] YieldParentGroups()
146150
/// because we need to minimally parse to see the anchors anchor validation is deferred.
147151
public IReadOnlyDictionary<string, string?>? AnchorRemapping { get; set; }
148152

153+
public IDirectoryInfo ScopeDirectory { get; set; }
154+
149155
private void ValidateAnchorRemapping()
150156
{
151157
if (AnchorRemapping is null)
@@ -224,9 +230,9 @@ protected void ReadDocumentInstructions(MarkdownDocument document)
224230

225231
var toc = GetAnchors(_set, MarkdownParser, YamlFrontMatter, document, subs, out var anchors);
226232

227-
_tableOfContent.Clear();
233+
_pageTableOfContent.Clear();
228234
foreach (var t in toc)
229-
_tableOfContent[t.Slug] = t;
235+
_pageTableOfContent[t.Slug] = t;
230236

231237

232238
foreach (var label in anchors)

src/Elastic.Markdown/IO/Navigation/DocumentationGroup.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,15 @@ internal DocumentationGroup(
112112
continue;
113113

114114

115-
foreach (var extension in context.Configuration.EnabledExtensions)
116-
extension.Visit(d, tocItem);
117-
118115
md.Parent = this;
119116
md.Hidden = file.Hidden;
120117
var navigationIndex = Interlocked.Increment(ref fileIndex);
121118
md.NavigationIndex = navigationIndex;
119+
md.ScopeDirectory = file.TableOfContentsScope.ScopeDirectory;
120+
121+
foreach (var extension in context.Configuration.EnabledExtensions)
122+
extension.Visit(d, tocItem);
123+
122124

123125
if (file.Children.Count > 0 && d is MarkdownFile virtualIndex)
124126
{
@@ -153,7 +155,7 @@ internal DocumentationGroup(
153155
children =
154156
[
155157
.. documentationFiles
156-
.Select(d => new FileReference(d.RelativePath, true, false, []))
158+
.Select(d => new FileReference(folder.TableOfContentsScope, d.RelativePath, true, false, []))
157159
];
158160
}
159161

src/Elastic.Markdown/Myst/InlineParsers/DiagnosticLinkInlineParser.cs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ public class DiagnosticLinkInlineParser : LinkInlineParser
5252
public override bool Match(InlineProcessor processor, ref StringSlice slice)
5353
{
5454
var match = base.Match(processor, ref slice);
55-
5655
if (!match || processor.Inline is not LinkInline link)
5756
return match;
5857

@@ -72,6 +71,9 @@ public override bool Match(InlineProcessor processor, ref StringSlice slice)
7271

7372
private static void ParseStylingInstructions(LinkInline link)
7473
{
74+
if (!link.IsImage)
75+
return;
76+
7577
if (string.IsNullOrWhiteSpace(link.Title) || link.Title.IndexOf('=') < 0)
7678
return;
7779

@@ -189,7 +191,16 @@ private static void ProcessInternalLink(LinkInline link, InlineProcessor process
189191
var includeFrom = GetIncludeFromPath(url, context);
190192
var file = ResolveFile(context, url);
191193
ValidateInternalUrl(processor, url, includeFrom, link, context);
192-
ProcessLinkText(processor, link, context, url, anchor, file);
194+
195+
if (link.IsImage && context.DocumentationFileLookup(context.MarkdownSourcePath) is MarkdownFile currentMarkdown)
196+
{
197+
//TODO make this an error once all offending repositories have been updated
198+
if (!file.Directory!.FullName.StartsWith(currentMarkdown.ScopeDirectory.FullName + Path.DirectorySeparatorChar))
199+
processor.EmitHint(link, $"Image '{url}' is referenced out of table of contents scope '{currentMarkdown.ScopeDirectory}'.");
200+
}
201+
202+
var linkMarkdown = context.DocumentationFileLookup(file) as MarkdownFile;
203+
ProcessLinkText(processor, link, linkMarkdown, anchor, url, file);
193204
UpdateLinkUrl(link, url, context, anchor, file);
194205
}
195206

@@ -214,14 +225,12 @@ private static void ValidateInternalUrl(InlineProcessor processor, string url, s
214225
processor.EmitError(link, $"`{url}` does not exist. resolved to `{pathOnDisk}");
215226
}
216227

217-
private static void ProcessLinkText(InlineProcessor processor, LinkInline link, ParserContext context, string url, string? anchor, IFileInfo file)
228+
private static void ProcessLinkText(InlineProcessor processor, LinkInline link, MarkdownFile? markdown, string? anchor, string url, IFileInfo file)
218229
{
219230
if (link.FirstChild != null && string.IsNullOrEmpty(anchor))
220231
return;
221232

222-
var markdown = context.DocumentationFileLookup(file) as MarkdownFile;
223-
224-
if (markdown == null && link.FirstChild == null)
233+
if (markdown is null && link.FirstChild == null)
225234
{
226235
processor.EmitWarning(link,
227236
$"'{url}' could not be resolved to a markdown file while creating an auto text link, '{file.FullName}' does not exist.");
@@ -234,7 +243,7 @@ private static void ProcessLinkText(InlineProcessor processor, LinkInline link,
234243
{
235244
if (markdown is not null)
236245
ValidateAnchor(processor, markdown, anchor, link);
237-
if (link.FirstChild == null && (markdown?.TableOfContents.TryGetValue(anchor, out var heading) ?? false))
246+
if (link.FirstChild == null && (markdown?.PageTableOfContent.TryGetValue(anchor, out var heading) ?? false))
238247
title += " > " + heading.Heading;
239248
}
240249

src/Elastic.Markdown/Slices/HtmlWriter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public async Task<string> RenderLayout(MarkdownFile markdown, MarkdownDocument d
9292
Title = markdown.Title ?? "[TITLE NOT SET]",
9393
TitleRaw = markdown.TitleRaw ?? "[TITLE NOT SET]",
9494
MarkdownHtml = html,
95-
PageTocItems = [.. markdown.TableOfContents.Values],
95+
PageTocItems = [.. markdown.PageTableOfContent.Values],
9696
Tree = DocumentationSet.Tree,
9797
CurrentDocument = markdown,
9898
PreviousDocument = previous,

0 commit comments

Comments
 (0)