Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5b283f9
Add legacy /guide AsciiDoc-to-Markdown migration tool
Mpdreamz Mar 19, 2026
5ed53e5
Fix migration output to pass docs-builder validation
Mpdreamz Mar 19, 2026
1aed4f3
Extract migration to isolated CLI tool and add missing parser features
Mpdreamz Mar 28, 2026
2fccb02
Refactor docs-migrate into subcommands and switch to bare-clone + wor…
Mpdreamz Mar 28, 2026
b66d80b
Fix conf.yaml parser missing nested section groups
Mpdreamz Mar 28, 2026
3a460b3
Normalize docs-migrate default work dir to AppData
Mpdreamz Apr 8, 2026
0569395
Write conversion output to cwd/.artifacts/migrated
Mpdreamz Apr 8, 2026
f54ad93
Add --no-hud flag to serve and fix branch resolution for repos withou…
Mpdreamz Apr 13, 2026
0b38331
Rewrite guide URLs to internal links and emit docs-builder substitutions
Mpdreamz Apr 13, 2026
a874035
Preserve block anchors as docs-builder inline anchors
Mpdreamz Apr 13, 2026
da2e415
Fix conditionals inside verbatim blocks, anchor map coverage, and sou…
Mpdreamz Aug 5, 2026
9bbe9d8
Migrate docs-migrate CLI to Nullean.Argh
Mpdreamz Aug 5, 2026
eda619f
Fix converter defects blocking elastic.co/guide book migration
Mpdreamz Aug 5, 2026
3a1904f
Fix 4 conversion quality bugs in MarkdownEmitter
Mpdreamz Aug 6, 2026
42276fd
docs-migrate: --minors flag, default port 3001, version URL routing fix
Mpdreamz Aug 6, 2026
e4efbfc
Fix xref display text, link attr substitution, and chunk level
Mpdreamz Aug 6, 2026
ee011cf
Fix xref conversion: inline regex ordering, bold/italic spanning, DL …
Mpdreamz Aug 6, 2026
2d01794
Inline include:: directives inside pipe tables
Mpdreamz Aug 6, 2026
fb7de4d
Fix callout collection: skip comments/blank after code blocks, add Pa…
Mpdreamz Aug 6, 2026
a2558f8
Fix multi-line callout descriptions and DL inline xref spanning
Mpdreamz Aug 6, 2026
fcb45ca
Load shared/attributes.asciidoc before converting each book
Mpdreamz Aug 6, 2026
407b7a6
Convert AsciiDoc typographic double quotes to straight quotes
Mpdreamz Aug 6, 2026
8340108
Fix chunking: each included file becomes a separate page regardless o…
Mpdreamz Aug 6, 2026
d9379fd
Fix: emit $$$anchor$$$ before H1 headings so docs-builder registers them
Mpdreamz Aug 6, 2026
020440d
Fix verbatim include resolution, ifeval stripping, and +..+ passthrou…
Mpdreamz Aug 6, 2026
892297b
ServeCommand: catch OperationCanceledException so Ctrl+C exits cleanly
Mpdreamz Aug 11, 2026
04293d2
Fix ifeval conditionals, trailing-space delimiters, and double-xref n…
Mpdreamz Aug 11, 2026
6769f1a
Fix: trim openingDelim in parser + route [sidebar] blocks correctly
Mpdreamz Aug 11, 2026
8b684c2
Fix: dotnet format whitespace in test files
Mpdreamz Aug 11, 2026
6eb675c
Fix table comment leak, long-dash code fence tests, and book filter p…
Mpdreamz Aug 11, 2026
9fd4a98
Regenerate CLI schema and fix Prettier formatting
Mpdreamz Aug 11, 2026
0955afa
fix: don't persist book filter across docs-migrate runs (#3835)
Mpdreamz Aug 13, 2026
523e76a
Add adoc-compare dev tool and register it in the solution
Mpdreamz Aug 13, 2026
b471a13
fix: add required license header to adoc-compare Program.cs
Mpdreamz Aug 13, 2026
0c9bfc5
Merge main into feature/ascii-to-md-converter, resolve conflicts
Mpdreamz Aug 13, 2026
7da63d0
Merge remote-tracking branch 'origin/main' into feature/ascii-to-md-c…
Mpdreamz Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
plans/
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Use the `/test` skill to pick the right test project automatically. A change to
| `src/Elastic.Documentation.Configuration/` | `dotnet test tests/Elastic.Documentation.Configuration.Tests/` |
| `src/Elastic.Documentation.Navigation/` | `dotnet test tests/Navigation.Tests/` (prefix dropped) |
| `src/Elastic.Documentation.Indexing/` | `dotnet test tests/Elastic.Documentation.Indexing.Tests/` |
| `src/authoring/Elastic.LegacyDocs.Migration/` | `dotnet test tests/Elastic.LegacyDocs.Migration.Tests/` |
| `src/tooling/essc/` | `dotnet test tests/Elastic.SiteSearch.Tests/` (essc's root namespace is `Elastic.SiteSearch.Cli`) |
| `src/Elastic.ApiExplorer/` | `dotnet test tests/Elastic.ApiExplorer.Tests/` |
| `src/Elastic.Documentation.Site/` | `cd src/Elastic.Documentation.Site && npm run test` |
Expand Down
4 changes: 4 additions & 0 deletions docs-builder.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
</Folder>
<Folder Name="/src/authoring/">
<Project Path="src/authoring/Elastic.Documentation.Refactor/Elastic.Documentation.Refactor.csproj" />
<Project Path="src/authoring/Elastic.LegacyDocs.Migration/Elastic.LegacyDocs.Migration.csproj" />
</Folder>
<Folder Name="/src/infra/">
<Project Path="src/infra/docs-lambda-index-publisher/docs-lambda-index-publisher.csproj" />
Expand All @@ -86,6 +87,8 @@
<File Path="src/tooling/Directory.Build.props" />
<Project Path="src/tooling/docs-builder/docs-builder.csproj" />
<Project Path="src/tooling/essc/essc.csproj" />
<Project Path="src/tooling/docs-migrate/docs-migrate.csproj" />
<Project Path="src/tooling/adoc-compare/adoc-compare.csproj" />
</Folder>
<Folder Name="/tests-integration/">
<File Path="tests-integration/Directory.Build.props" />
Expand All @@ -111,6 +114,7 @@
<Project Path="tests/Elastic.Documentation.Integrations.Tests/Elastic.Documentation.Integrations.Tests.csproj" />
<Project Path="tests/Elastic.SiteSearch.Tests/Elastic.SiteSearch.Tests.csproj" />
<Project Path="tests/Elastic.Documentation.OpenApiIndex.Tests/Elastic.Documentation.OpenApiIndex.Tests.csproj" />
<Project Path="tests/Elastic.LegacyDocs.Migration.Tests/Elastic.LegacyDocs.Migration.Tests.csproj" />
</Folder>
<Project Path=".github/.github.csproj">
<Build Project="false" />
Expand Down
8 changes: 8 additions & 0 deletions docs/cli-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,14 @@
"summary": "Special flag for dotnet watch optimizations during development",
"defaultValue": "false"
},
{
"role": "flag",
"name": "no-hud",
"type": "boolean",
"required": false,
"summary": "Disable the diagnostics HUD and background validation builds",
"defaultValue": "false"
},
{
"role": "flag",
"name": "log-level",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@ public ConfigurationFile(DocumentationSetFile docSetFile, IDocumentationSetConte
_features["primary-nav"] = docSetFile.Features.PrimaryNav.Value;
if (docSetFile.Features.DisableGithubEditLink.HasValue)
_features["disable-github-edit-link"] = docSetFile.Features.DisableGithubEditLink.Value;
if (docSetFile.Features.GuideNav.HasValue)
_features["guide-nav"] = docSetFile.Features.GuideNav.Value;

// primary-nav requires the Elastic global navigation which is not available for white-label builds
if (Branding is not null && docSetFile.Features.PrimaryNav is true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ public bool NavigationPreviewEnabled
set => _featureFlags["navigation-preview"] = value;
}

public bool GuideNavEnabled
{
get => IsEnabled("guide-nav");
set => _featureFlags["guide-nav"] = value;
}

private bool IsEnabled(string key)
{
var envKey = $"FEATURE_{key.ToUpperInvariant().Replace('-', '_')}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,8 @@ public class DocumentationSetFeatures
public bool? PrimaryNav { get; set; }
[YamlMember(Alias = "disable-github-edit-link", ApplyNamingConventions = false)]
public bool? DisableGithubEditLink { get; set; }
[YamlMember(Alias = "guide-nav", ApplyNamingConventions = false)]
public bool? GuideNav { get; set; }
}

[YamlSerializable]
Expand Down
203 changes: 203 additions & 0 deletions src/authoring/Elastic.LegacyDocs.Migration/ArchiveDocsetGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Licensed to Elasticsearch B.V under one or more agreements.
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
// See the LICENSE file in the project root for more information

using Elastic.LegacyDocs.Migration.Asciidoc;
using Microsoft.Extensions.Logging;

namespace Elastic.LegacyDocs.Migration;

public record ArchiveGeneratorOptions
{
public required string OutputDirectory { get; init; }
public string? BookFilter { get; init; }
public bool AllVersions { get; init; }
public int? MinMajorVersion { get; init; }
public required SourceRepoManager RepoManager { get; init; }
}

public class ArchiveDocsetGenerator(ILogger<ArchiveDocsetGenerator> logger)
{
public async Task GenerateAsync(LegacyConf conf, ArchiveGeneratorOptions options, CancellationToken ct = default)
{
var books = conf.Contents
.SelectMany(c => c.Sections)
.Where(b => options.BookFilter is null || b.Prefix == options.BookFilter)
.ToList();

logger.LogInformation("Processing {BookCount} books in archive mode", books.Count);

var tocRefs = new List<string>();

foreach (var book in books)
{
ct.ThrowIfCancellationRequested();

var versions = GetVersionsToProcess(book, options.AllVersions, options.MinMajorVersion);
if (versions.Count == 0)
{
logger.LogWarning("No versions to process for {BookPrefix}", book.Prefix);
continue;
}

logger.LogInformation("Book {Prefix}: {Count} versions to process", book.Prefix, versions.Count);

var prefixDir = Path.Combine(options.OutputDirectory, book.Prefix);
var versionEntries = new List<TocEntry>();

foreach (var version in versions)
{
ct.ThrowIfCancellationRequested();

var versionLabel = version.VersionLabel;
try
{
var pages = await ProcessBookVersion(book, version, options, ct);
if (pages.Count == 0)
continue;

var versionDir = Path.Combine(prefixDir, versionLabel);
_ = Directory.CreateDirectory(versionDir);

var fileEntries = await WritePages(pages, versionDir, ct);
YamlWriter.WriteTocYaml(Path.Combine(versionDir, "toc.yml"), fileEntries);
versionEntries.Add(new TocEntry { Folder = versionLabel });

logger.LogInformation("Wrote {PageCount} pages for {Prefix}/{Version}",
pages.Count, book.Prefix, versionLabel);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Failed to process {Prefix}/{Version}", book.Prefix, versionLabel);
}
}

if (versionEntries.Count > 0)
{
tocRefs.Add(book.Prefix);
YamlWriter.WriteTocYaml(Path.Combine(prefixDir, "toc.yml"), versionEntries);
}
}

if (tocRefs.Count > 0)
YamlWriter.WriteDocsetYaml(Path.Combine(options.OutputDirectory, "docset.yml"), "guide-archive", tocRefs);

logger.LogInformation("Archive generation complete: {BookCount} books", tocRefs.Count);
}

private async Task<IReadOnlyList<PageOutput>> ProcessBookVersion(
LegacyBook book, BranchRef version, ArchiveGeneratorOptions options, CancellationToken ct)
{
var versionLabel = version.VersionLabel;
var sources = await options.RepoManager.ResolveSourcesAsync(book, version, ct);
if (sources.Count == 0)
{
logger.LogWarning("No sources resolved for {Prefix} version {Version}", book.Prefix, versionLabel);
return [];
}

var primarySource = sources[0];
var indexPath = Path.Combine(primarySource.LocalPath, book.Index);
if (!File.Exists(indexPath))
{
logger.LogWarning("Index file not found: {IndexPath}", indexPath);
return [];
}

var content = await File.ReadAllTextAsync(indexPath, ct);
var basePath = Path.GetDirectoryName(indexPath) ?? primarySource.LocalPath;
var parserOptions = new AsciidocParserOptions
{
Attributes = new Dictionary<string, string>
{
["branch"] = versionLabel,
["doc-tests-src"] = primarySource.LocalPath
}
};
var parser = new AsciidocParser(parserOptions);
var document = parser.Parse(content, basePath);

var emitterOptions = new MarkdownEmitterOptions
{
BookPrefix = book.Prefix,
Version = versionLabel
};
var emitter = new MarkdownEmitter(emitterOptions);

return PageChunker.Chunk(document, book.Chunk, emitter);
}

private static async Task<List<TocEntry>> WritePages(
IReadOnlyList<PageOutput> pages, string directory, CancellationToken ct)
{
var entries = new List<TocEntry>();
foreach (var page in pages)
{
var filename = $"{page.Slug}.md";
await File.WriteAllTextAsync(Path.Combine(directory, filename), page.MarkdownContent, ct);
entries.Add(new TocEntry { File = filename });
}
return entries;
}

internal static List<BranchRef> GetVersionsToProcess(LegacyBook book, bool allVersions, int? minMajorVersion = null)
{
var branches = FilterByMinVersion(book.Branches, minMajorVersion);

if (allVersions)
return SortBranchesDescending(branches);

var selected = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

if (!string.IsNullOrEmpty(book.Current))
_ = selected.Add(book.Current);

var grouped = branches
.Select(b => (Branch: b, Parsed: TryParseMajorMinor(b.VersionLabel)))
.Where(x => x.Parsed.HasValue)
.GroupBy(x => x.Parsed!.Value.Major);

foreach (var group in grouped)
{
var topTwo = group
.OrderByDescending(x => x.Parsed!.Value.Minor)
.Take(2);

foreach (var (branch, _) in topTwo)
_ = selected.Add(branch.VersionLabel);
}

return SortBranchesDescending(branches.Where(b => selected.Contains(b.VersionLabel)));
}

private static List<BranchRef> FilterByMinVersion(IEnumerable<BranchRef> branches, int? minMajor)
{
if (minMajor is null)
return branches.ToList();

return branches
.Where(b =>
{
var parsed = TryParseMajorMinor(b.VersionLabel);
return parsed.HasValue && parsed.Value.Major >= minMajor;
})
.ToList();
}

private static List<BranchRef> SortBranchesDescending(IEnumerable<BranchRef> branches) =>
branches
.Select(b => (Branch: b, Parsed: TryParseMajorMinor(b.VersionLabel)))
.OrderByDescending(x => x.Parsed?.Major ?? 0)
.ThenByDescending(x => x.Parsed?.Minor ?? 0)
.Select(x => x.Branch)
.ToList();

private static (int Major, int Minor)? TryParseMajorMinor(string version)
{
var parts = version.Split('.');
if (parts.Length >= 2 && int.TryParse(parts[0], out var major) && int.TryParse(parts[1], out var minor))
return (major, minor);

return null;
}
}
Loading
Loading