AsciiDoc → Markdown converter for elastic.co/guide books - #3802
Draft
Mpdreamz wants to merge 25 commits into
Draft
AsciiDoc → Markdown converter for elastic.co/guide books#3802Mpdreamz wants to merge 25 commits into
Mpdreamz wants to merge 25 commits into
Conversation
Implements a native C# converter to migrate elastic.co/guide content (AsciiDoc served by the legacy elastic/docs system) into Markdown docsets that docs-builder can render. New project: Elastic.LegacyDocs.Migration with: - conf.yaml parser for the elastic/docs book definitions - Source repo manager for resolving branches and paths - AsciiDoc lexer, parser, and AST (sections, code blocks, tables, admonitions, lists, includes, conditionals, inline formatting) - Markdown emitter targeting MyST syntax - Page chunker to split documents by section level - Archive and Latest docset generators producing docset.yml/toc.yml - CLI command: `docs-builder guide migrate` Made-with: Cursor
Fixes found by running the converter end-to-end and building with docs-builder: - Fix section level off-by-one in lexer (= is level 0, == is level 1) - Fix Repos model to handle nested YAML objects (Dict<string, LegacyRepo>) - Fix basePath in generators to use index file directory for includes - Fix chunker to walk into level-0 sections and promote chunked sections to H1 for standalone pages - Use docs-builder anchor syntax (# Title [anchor]) instead of MyST (anchor)= targets - Map unsupported directives: caution→warning, sidebar→admonition - Add cross-page anchor-to-slug map so refs emit file links - Add bare URL[text] pattern to inline parser - Prevent double image path prefix Made-with: Cursor
- Add DSV/CSV/TSV table format support (384 occurrences in ES|QL docs) - Add pass:[] and +inline+ passthrough inline patterns - Add unconstrained bold (**text**) support - Add [role]#text# inline macro support - Extract migration command into standalone docs-migrate CLI tool (removable by deleting src/tooling/docs-migrate/ and src/authoring/Elastic.LegacyDocs.Migration/) - Remove migration coupling from docs-builder Made-with: Cursor
…ktree strategy - Split monolithic MigrateCommand into init/list/clone/convert/serve subcommands - SourceRepoManager now uses bare clones + git worktrees + sparse checkout (cone mode) to avoid redundant full clones per branch and only fetch needed directory trees - Add Proc package for process execution in place of raw System.Diagnostics.Process - Add guide-nav feature flag to configuration layer (FeatureFlags, DocumentationSetFile, ConfigurationFile) so converted docsets can signal the guide-archive nav mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The conf.yaml structure has up to 3 levels of nesting: a category's sections can contain sub-groups (with base_dir + sections) rather than books directly. LegacyBook had no Sections property so IgnoreUnmatchedProperties silently dropped all nested books, causing entire families to disappear (Elasticsearch clients, all APM agents, ECS logging, etc.). Add BaseDir and Sections to LegacyBook, then recursively flatten after deserialization, accumulating the base_dir prefix so leaf book prefixes are fully qualified (e.g. en/elasticsearch/client/net-api). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Use LocalApplicationData/elastic/docs-migrate as the default work directory, matching the pattern from Paths.ApplicationData in the main codebase. Includes the same Docker/CI fallback to GetTempPath() when LocalApplicationData returns an empty string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Output from convert and serve now lands in .artifacts/migrated relative to the current working directory, separate from the AppData work dir that holds conf.yaml and repo clones. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t versioned branches - Add --no-hud to docs-builder serve: disables diagnostics HUD and skips background in-memory validation builds - docs-migrate serve passes --no-hud automatically - Fix SourceRepoManager branch resolution: check local branches before fetching, use --detach for worktrees so repos like elastic/docs (only has master) can serve multiple version worktrees - Use Proc.Start for branch probing to suppress expected git errors Made-with: Cursor
- Rewrite https://www.elastic.co/guide/... URLs to absolute internal markdown links (/prefix/version/page.md) so docs-builder validates them - Emit {{name}} (double braces) for unresolved AsciiDoc attribute references so docs-builder recognizes them as substitution variables - Add SharedAttributes with ~130 well-known product-name attributes from shared/attributes.asciidoc (es, kib, agent, fleet, etc.) - Write subs: section in generated docset.yml with all product names Made-with: Cursor
AsciiDoc [[id]] anchors on non-section blocks (paragraphs, tables, description lists, etc.) were silently discarded. Now they are wrapped in an AnchoredBlock node and emitted as $$$id$$$ inline anchors that docs-builder renders as <a id="..."></a> link targets. Made-with: Cursor
…rce attributes - Fix lexer: conditionals inside verbatim/table blocks were being parsed as conditional tokens instead of raw text; now only recognized outside those contexts - Fix table delimiter regex to accept |=== with 3+ = signs - Fix parser: attribute substitution in section titles and image alt/title; preserve pending id/title/blockAttr across list-continuation blocks; track pendingStart to reset pos on section-level break - Fix page chunker: include AnchoredBlock IDs in anchor-to-slug map - Fix ConvertCommand: pass source_branch and per-repo root attributes to parser; guard against missing Current version when building overview Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs-migrate was the last CLI still on ConsoleAppFramework, which had no PackageVersion entry and so failed restore under Central Package Management. Align it with docs-builder and essc: Host.CreateApplicationBuilder + AddArgh, app.Map<T>() registration, and CancellationToken in place of Cancel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes eight root-cause bugs that produced garbled output across 21,962+
files in the converted corpus. All changes are covered by a new
tests/Elastic.LegacyDocs.Migration.Tests/ project (19 tests).
Step 0: Add test project, diagnostics channel (OnDiagnostic on
AsciidocParserOptions), and AGENTS.md test-project table row.
Step 1: Fix `--` open blocks — lexer dispatched on delimChar before
checking length so `--` fell into the verbatim path. Now length < 4
routes to BlockDelimiter; IsMatchingDelimiter/IsMatchingClose require
exact length so `--------` can no longer close a `----` block.
PageChunker now recurses into OpenBlockNode for anchor collection and
page extraction.
Step 2: Add IncludeDirective case to ParseBlock's switch — includes
were silently skipped (SkipToken) instead of being processed.
Step 3: Attribute resolution — eager expansion via SetAttribute helper,
SetBasePath keeps docdir in sync, Path.GetFullPath normalization in
ProcessInclude fixes 863 `../../shared/attributes.asciidoc` paths,
docs-root/asciidoc-dir seeded in ConvertCommand, docdir saved and
restored across include file boundaries.
Step 4: Emitter substitution — undefined attributes emit `{name}` not
`{{name}}`; ProductNames keys pass through as `{{name}}` for
docs-builder subs. SetAttribute skips ProductNames so shared/
attributes.asciidoc cannot shadow them.
Step 5: Callout annotations — populate CodeBlockNode.Callouts from
trailing `<n> text` lines after a code block closing delimiter,
making the emitter's ordered-list output live.
Step 6: include-tagged:: — new ResolveVerbatimIncludes handles
`include-tagged::` directives inside listing blocks; tag/end regexes
widened to allow hyphens and drop the //prefix requirement.
Step 7: Xref regex uses non-greedy `.+?` to allow `>` in link text;
multi-line admonition paragraphs collect continuation Text tokens
instead of truncating at the first line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Headings: apply attribute substitution to document/section titles so
{es} → {{es}} rather than leaking as raw text
- Same-page xrefs: compare resolved anchor slug against current page
slug; emit #anchor for same-page and slug.md#anchor for cross-page
- AnchoredBlock: emit a blank line after $$$anchor$$$ so docs-builder
treats it as a standalone inline anchor block
- titleabbrev: PassthroughNode wrapping <titleabbrev>…</titleabbrev>
was missed by the ParagraphNode filter; add the check to the
PassthroughNode emit branch
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add --minors N to clone and convert commands to cap minor versions per major; stored in FilterOptions and persisted to .clone-options.json - Change docs-migrate serve default port from 3000 to 3001 - Fix /8.19 URL routing: Path.GetExtension treats version segments like 8.19 as having extension .19; restrict hasKnownExtension check to actual document file types so version-number slugs resolve correctly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Build AnchorToTitleMap alongside AnchorToSlugMap in PageChunker;
when a section has <titleabbrev>, use that as the display title
(matches the abbreviated nav title on elastic.co, e.g. "Indices and
documents" vs full "Indices, documents, and fields")
- EmitCrossRef: resolve <<anchor>> with no text to the section title
from AnchorToTitleMap, not the raw anchor ID
- EmitLink: apply SubstituteTitleAttrs to link text so {es}//{kib}
references in URL link text become {{es}}/{{kib}} instead of leaking
as single-brace literals
- ConvertCommand: conf.yaml chunk:N maps to chunkLevel N+1 in our AST
(chunk:1 → chunkLevel 2 creates separate pages for === sections,
matching elastic.co's page-per-section structure; 423 → 3376 pages)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…multi-line collection, title xrefs - Prevent double-backtick AsciiDoc curly-quotes from opening a code span by adding (?<!`) lookbehind on the mono pattern (group 16) - Restrict bold (groups 13,14) and italic (group 15) from matching across < so they cannot consume <<anchor>> xref openers in cross-mark content (e.g. *word*<<xref>>) - Fix ParseDescriptionList to collect consecutive Text tokens into one paragraph (matching ParseParagraph behaviour) so xrefs that wrap across continuation lines within a DL item are kept in a single ParseInlines call - Add SubstituteTitleXrefs helper to MarkdownEmitter and apply it to section headings, document titles, and callout descriptions, which previously used SubstituteTitleAttrs only and left <<anchor,text>> patterns unconverted - Add ifeval:: lexer fix: relax middle group from .+ to .* to allow empty attribute name - Add GetBlockAttributeRegex trailing-whitespace trim to avoid false negatives - Add test coverage: curly-quote context, ordered-list items, bold-asterisk interference Result: raw xref leftovers in en/elasticsearch/reference/8.19 drop from 273 to 9 files. The remaining 9 are source-level bugs (malformed single >), code-block placeholders, or xrefs genuinely inside code spans -- all correct not-converted behaviour. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a pipe table contains an include:: on its own line (typical
for shared common-options snippets), the lexer emits it as a Text
token (not IncludeDirective) because the inTable state flag is not
set during include resolution. ParseTable now detects the include::
pattern in Text tokens, reads the referenced file, and appends its
| ... | rows directly to the current table instead of leaking the
directive text into the last cell.
Also handles the IncludeDirective token type for future-proofing.
Result: include:: leftovers drop from 58 to 24 (remaining 24 are
{esql-specs} references to external test fixture files that are not
part of the repository and cannot be resolved).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rseCalloutList for orphan markers - After a listing block, skip any trailing comment tokens (// TEST[...]) and one optional blank line before attempting callout collection; restore position if no callout markers follow, preventing skipped blank lines from orphaning later content - Add ParseCalloutList routed from ParseBlock for standalone <n> callout lines that appear after continuation text or in other contexts where the code-block collector already broke; collects multi-line descriptions via continuation join - Lexer: relax ifeval:: middle group .+ → .* to allow empty attribute name - Lexer: allow trailing whitespace on block attribute lines [attr]\s* Result: raw <n> callout markers in en/elasticsearch/reference/8.19 drop from ~396 to ~90 files. Remaining cases are multi-callout blocks where <1> description wraps across lines, breaking the code-block collector before reaching <2>+. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ParseParagraph: stop collecting Text tokens at callout markers (<n>) so ParseCalloutList gets to handle the run instead of having them swallowed into paragraph content - ParseDescriptionList: same guard in the inline Text-continuation loop - Code-block callout collector: collect continuation lines for each <n> description so multi-line callout text is fully captured rather than truncated at the first line - ParseDescriptionList: when inline description text (after ::) contains an unclosed <<, join continuation Text tokens until the xref closes, enabling multi-line xref spanning within DL items Result: callout markers drop from 90 to 0; residual raw xrefs drop from 23 to 22 all of which are either inside code spans/fences (correct behaviour) or source-level malformed >> (not fixable). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pre-parse docs-repo/shared/attributes.asciidoc using the existing AsciidocParser
with branch/version seed attributes already set, so feature and product-name
abbreviations like {transform}, {ilm-init}, {anomaly-detect} and cross-guide
URL attributes like {ref}, {logstash-ref} are resolved at conversion time.
ProductNames keys (es, kib, etc.) are still excluded from eager expansion so they
continue to emit as {{name}} docs-builder substitution placeholders that get
resolved from the generated docset.yml subs: block.
Adds AsciidocParser.LoadAttributeFile static helper and ResolvedAttributes
property to support this pre-parse pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add group 23 to InlineCombinedRegex to match ``text'' (AsciiDoc opening double-backtick + closing double-apostrophe) and emit as "text" (straight double-quote pair). The existing lookbehind on the mono group already prevents `` from opening a code span, so group 23 matches the remaining cases that fall through to raw text. Eliminates 21 occurrences of visible AsciiDoc typographic quote syntax in en/elasticsearch/reference/8.19. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…f section level - Add IsIncludeRoot flag to SectionNode to mark sections produced at the top level of a ProcessInclude call - Set IsIncludeRoot=true in ParseSection's IncludeDirective handler and in ProcessInclude's main loop - Fix chunkLevel calculation: conf.yaml chunk:N maps directly to AST level N, so pass book.Chunk directly instead of book.Chunk+1 - Fix ExtractPages to recurse into inline sections (level > chunkLevel) when looking for nested IsIncludeRoot sections — this handles the case where an include appears after a discrete section, which nests the included content under the discrete section in the AST - Fix CollectAnchors to also recurse into inline sections for the same reason - Add IncludeChain_EachIncludedFile_BecomesASeparatePage test that mirrors the elastic.co search-your-data structure with 3-level include nesting and discrete sections Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs-builder's anchor registry includes H2+ heading slugs and $$$...$$$ inline anchors but NOT H1 heading slugs. Cross-references like [text](page.md#page-anchor-id) were triggering 96 'does not exist' errors because the page-level anchor (from the original [[block-anchor]] before the first section) was emitted as '# Title [#anchor-id]' — a format docs-builder parses for H2+ but does not register in the anchor collection for H1. Fix: emit $$$anchor-id$$$ on a standalone line before H1 headings (both from document.Title and from Level=0 SectionNode). The $$$...$$$ syntax is the docs-builder InlineAnchor format, which is collected via document.Descendants<InlineAnchor>() and included in markdown.Anchors. With this fix: 0 errors and 0 warnings from docs-builder serve. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Label error. Requires exactly 1 of: automation, breaking, bug, changelog:skip, chore, ci, dependencies, documentation, enhancement, feature, fix, redesign. Found: |
…gh bug - Extend ResolveVerbatimIncludes to handle standard include::[tag=] and full-file include::[] directives inside verbatim blocks (fixes 164 ESQL pages) - Strip ifeval::/ifdef::/endif:: conditional markers from verbatim block content where they can't be evaluated (fixes 2 files) - Add lookbehind to constrained +..+ passthrough regex so '8.0+' and similar version indicators don't start a passthrough span (fixes xref leaking past the passthrough into prose) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LF2iCfVK3ecJgPeFCP6p6S
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
docs-migrate, a standalone CLI that converts the legacy elastic.co/guide AsciiDoc corpus into docs-builder Markdown docsets. Each converted book becomes a self-contained docset that can be served immediately withdocs-builder serve.shared/attributes.asciidocloaded before each book so product-name substitutions resolve$$$anchor$$$inline anchors so docs-builder cross-reference validation passeselastic.co/docsequivalentsinclude::andinclude-tagged::directives inside verbatim blocks are resolved (covers ESQL spec pages)ifeval/ifdef/endifmarkers inside verbatim content are strippedGetting started
Prerequisites
Full corpus (all books, latest major + one minor)
Single book — Elasticsearch reference 8.19 only
Use
listto browse available book prefixes:Output lands in
.artifacts/migrated/relative to the working directory (defaults to the OS app-data folder; override with--work-dir).What's still open
include::inside|===table blocks is not expanded; ESQL function pages show the query example but not the result table