Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,31 @@ The site uses [DocFX](https://dotnet.github.io/docfx/) and GitHub flavoured mark

All contributions are welcome. We will review all pull requests submitted.

To test your changes locally:
- Make sure [DocFX](https://dotnet.github.io/docfx/) and Python 3.11+ are installed.
- Run `python build-docs.py --serve` in the root of the project.
For convenience for typical contributions, we have built a simple wrapper around the build process.
Unless you are working specifically on localization or the build process itself, you should be able to get by with the `run` script.

# Build Script Usage
Getting started:
1. Make sure you have Bash installed (included in most Linux distros, macOS, and [Git for Windows](https://git-scm.com/install/windows))
2. From the repo root, run the setup check and install any tools it reports as missing
- (Linux distros and macOS): `./run setup`
- (Windows): `bash run setup` (after this run everything from Git Bash)
3. To iterate on docs and see a preview in your browser:
- in one terminal: `./run serve`: launches a localhost server that renders the docs at http://localhost:8080 where you can see the docs as they will be rendered on the docs website
- in another terminal: `./run watch`: regenerates the rendered site every time you save a change to a markdown document, so you can refresh your browser to see it

These commands all build and work only on the English language docs, as the English markdown is our canonical version of documentation and the language we expect contributions in.

For more info, try `./run help` and `./run <subcommand> help`.
If you'd like more detail, [check out the run script README](build_scripts/run_scripts/README.md).

If you want to have more control over the build process, continue reading below about the `build-docs.py` script.

# Advanced build Script Usage

The `build-docs.py` script handles all documentation building tasks including multi-language support.
Make sure you have Python >=3.11 and docfx installed, the latter either globally or locally.

## Quick Start
## Using build-docs.py directly

```bash
# Build and serve locally (English only, for development)
Expand Down Expand Up @@ -50,6 +66,8 @@ swa start _site
| `--serve` | Build and serve locally (English only, for development) |
| `--skip-gen` | Skip running gen_redirects.py (use existing configs) |
| `--no-api-copy` | Skip copying API docs to localized sites |
| `--skip-api` | Reuse existing API metadata in content/api, ~30-40% faster (local markdown iteration only, requires `--serve`/`--lang`; never for testing/CI/CD/releases) |
| `--permissive` | Don't treat English DocFX warnings as build failures (for local iteration; full/CI builds stay strict) |
| `--sync` | Sync English fallback for missing/outdated translations (for local dev) |

## What the Build Script Does
Expand All @@ -68,16 +86,24 @@ swa start _site
# Project Structure

```
TEDoc/
/
├── build-docs.py # Main build script
├── run # Task runner for common dev tasks (see build_scripts/run_scripts/README.md)
├── build_scripts/ # Helper scripts
│ ├── gen_redirects.py # Generates docfx.json configs
│ ├── check_links.py # Dead-link checker for the generated _site
│ ├── config_loader.py # Shared configuration loader for build scripts
│ ├── csharp_doctest.py # Validates annotated C# code blocks in docs against the te CLI
│ ├── gen_languages.py # Generates language manifest
│ ├── gen_staticwebapp_config.py
│ ├── inject_seo_tags.py
│ ├── sync-localized-content.py
│ ├── gen_redirects.py # Generates docfx.json configs
│ ├── gen_sitemap_index.py # Post-processes the English sitemap; generates the sitemap index
│ ├── gen_staticwebapp_config.py # Generates Azure Static Web Apps routing config
│ ├── inject_seo_tags.py # Adds hreflang and canonical tags to built HTML
│ ├── normalize-localized-alerts.py # Repairs Crowdin-collapsed DocFX alerts
│ └── normalize-localized-heading-anchors.py # Injects English-slug bookmark anchors into translations
│ ├── normalize-localized-heading-anchors.py # Injects English-slug bookmark anchors into translations
│ ├── sync-localized-content.py # Syncs English content into localized build dirs
│ ├── te_script_runner.py # Runs C# snippets against a throwaway model via the te CLI
│ ├── test-fixtures/ # Fixtures for the build-script tests
│ └── run_scripts/ # ./run subcommand scripts and shared lib.sh (see its README)
├── content/ # English source content (tracked in git)
│ └── _ui-strings.json # English UI strings (header, footer, banners)
├── localizedContent/ # Build directories for all languages
Expand Down Expand Up @@ -222,4 +248,3 @@ If a key is missing from a language's file, or no `_ui-strings.json` exists at a
| `tableOfContents` | `Table of Contents` | Mobile TOC offcanvas title |
| `selectLanguage` | `Select language` | Language picker label |
| `copyCode` | `Copy code` | Code block copy button aria-label |

30 changes: 22 additions & 8 deletions build_scripts/check_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ def fetch_external(url: str, need_body: bool) -> FetchResult:
`_GET_ONLY_HOSTS`). GET downloads the body only when a fragment must be verified."""
target = "https:" + url if url.startswith("//") else url # protocol-relative //host needs a scheme
get_only = need_body or _host_of(url) in _GET_ONLY_HOSTS
for method in (("GET",) if get_only else ("HEAD", "GET")):
for method in ("GET",) if get_only else ("HEAD", "GET"):
request = urllib.request.Request(target, method=method, headers={"User-Agent": _USER_AGENT})
try:
with urllib.request.urlopen(request, timeout=20) as response:
Expand Down Expand Up @@ -642,7 +642,7 @@ def map_to_source(built: str, root: Path) -> str | None:
def _anchor_member(type_stem: str, fragment: str) -> str:
"""Best-effort member name from a DocFX member anchor; empty when the anchor scheme does not match."""
prefix = type_stem.replace(".", "_") + "_"
return fragment[len(prefix):].split("_", 1)[0] if fragment.startswith(prefix) else ""
return fragment[len(prefix) :].split("_", 1)[0] if fragment.startswith(prefix) else ""


def _fragment_suffix(target: Target) -> str:
Expand Down Expand Up @@ -826,16 +826,20 @@ def rank(item: tuple[str, HostStats]) -> tuple[int, int, int, float]:
return outcome.broken, outcome.partial, stat.rate_limited, stat.wait_seconds

print(f"\n--- stats ({elapsed:.1f}s, {len(active)} hosts) ---")
print(f"{'total':>5} {'ok':>5} {'frag':>5} {'bad':>5} {'reqs':>5} {'429':>4} {'401':>4} {'403':>4} {'404':>4} "
f"{'oth':>4} {'net':>4} {'wait(s)':>8} {'fb':>4} {'cap':>4} host [retry-after seen]")
print(
f"{'total':>5} {'ok':>5} {'frag':>5} {'bad':>5} {'reqs':>5} {'429':>4} {'401':>4} {'403':>4} {'404':>4} "
f"{'oth':>4} {'net':>4} {'wait(s)':>8} {'fb':>4} {'cap':>4} host [retry-after seen]"
)
for host, stat in sorted(active.items(), key=rank, reverse=True):
oc = outcomes.get(host, empty)
seen = sorted(stat.retry_after_seen)
tail = f" {seen}" if stat.rate_limited else ""
print(f"{oc.total:>5} {oc.ok:>5} {oc.partial:>5} {oc.broken:>5} {stat.requests:>5} {stat.rate_limited:>4} "
f"{oc.status.get(401, 0):>4} {oc.status.get(403, 0):>4} {oc.status.get(404, 0):>4} "
f"{oc.other_http:>4} {oc.transport:>4} {stat.wait_seconds:>8.1f} "
f"{stat.head_fallbacks:>4} {stat.final_cap:>4} {host}{tail}")
print(
f"{oc.total:>5} {oc.ok:>5} {oc.partial:>5} {oc.broken:>5} {stat.requests:>5} {stat.rate_limited:>4} "
f"{oc.status.get(401, 0):>4} {oc.status.get(403, 0):>4} {oc.status.get(404, 0):>4} "
f"{oc.other_http:>4} {oc.transport:>4} {stat.wait_seconds:>8.1f} "
f"{stat.head_fallbacks:>4} {stat.final_cap:>4} {host}{tail}"
)


def cmd_validate(args: list[str], progress: _Progress, base_url: "Callable[[], str]") -> int:
Expand All @@ -851,10 +855,20 @@ def cmd_validate(args: list[str], progress: _Progress, base_url: "Callable[[], s
under = next((arg[len("under=") :] for arg in args if arg.startswith("under=")), None)
positional = [arg for arg in args if arg not in flags and not arg.startswith("under=")]
root = Path(positional[0]) if positional else Path("_site")
if not root.is_dir():
# A missing root would otherwise scan zero pages and "pass"; that
# silence would hide a forgotten or failed build.
print(f"error: site root not found: {root} (build the site first)", file=sys.stderr)
return 2
source_prefix = os.path.normpath(root / under) if under else None

progress.phase = "enumerating"
refs, anchors_by_file = enumerate_site(root, source_prefix, progress)
if progress.pages == 0:
# Same silence risk as a missing root: a site with no HTML means
# nothing was validated, not that nothing is broken.
print(f"error: no HTML pages found under {root} (build the site first)", file=sys.stderr)
return 2
existing = _existing_files(root)
progress.phase = "resolving"
# Resolve the base URL only when needed: local mode never checks live, so it stays offline and config-free.
Expand Down
3 changes: 1 addition & 2 deletions build_scripts/csharp_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,8 +352,7 @@ def _classify_fence(fence: Fence, following: Fence | None) -> Block | None:
first = fence.annotation.split(None, 1)[0] if fence.annotation else ""
if first in ("compile", "run"):
raise ValueError(
f"{first} annotation is only valid on a csharp block "
f"(line {fence.line}, lang={fence.lang or 'plain'})"
f"{first} annotation is only valid on a csharp block (line {fence.line}, lang={fence.lang or 'plain'})"
)
return None
annotation = fence.annotation
Expand Down
Loading
Loading