diff --git a/.github/workflows/check-tutorial-sync.yml b/.github/workflows/check-tutorial-sync.yml new file mode 100644 index 000000000..52369b060 --- /dev/null +++ b/.github/workflows/check-tutorial-sync.yml @@ -0,0 +1,26 @@ +name: Check tutorial code sync +on: + workflow_dispatch: + pull_request: + paths: + - 'docs/tutorials/**' + - 'scripts/tutorial-sync/**' + +jobs: + check-sync: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: dashpay/platform-tutorials + path: platform-tutorials + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.10' + + - run: pip install pyyaml + + - run: python3 scripts/tutorial-sync/sync_tutorial_code.py --check --source platform-tutorials diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..ad80b25a9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is the Dash Platform documentation repository - a Sphinx-based documentation site that covers Dash Platform features, APIs, SDKs, and developer guides. The documentation is written in Markdown with MyST parser extensions and built with Sphinx. + +## Build Commands + +```bash +# Set up development environment +python -m venv venv +source venv/bin/activate # or venv\Scripts\activate on Windows +pip install -r requirements.txt + +# Build documentation +make html + +# Clean build +make clean + +# Sync sidebar after building (recommended after adding new pages) +python scripts/sync_sidebar.py + +# Sync tutorial code from platform-tutorials repo +python3 scripts/tutorial-sync/sync_tutorial_code.py --source /path/to/platform-tutorials + +# Check for tutorial code drift (CI mode) +python3 scripts/tutorial-sync/sync_tutorial_code.py --check --source /path/to/platform-tutorials + +# View built documentation +# Open _build/html/index.html in browser +``` + +## Architecture + +- **conf.py**: Main Sphinx configuration with extensions, theme settings, and intersphinx mappings +- **docs/**: Main documentation content organized by sections: + - `intro/`: Background and platform overview + - `tutorials/`: Step-by-step guides + - `explanations/`: Feature descriptions and concepts + - `reference/`: API documentation and technical specs + - `protocol-ref/`: Platform protocol reference + - `sdk-js/`, `sdk-rs/`: SDK documentation +- **_templates/**: Custom Jinja2 templates for sidebar and layout +- **_static/**: CSS, JavaScript, and image assets +- **scripts/**: Utility scripts including sidebar synchronization + +## Documentation Structure + +The site uses a hierarchical structure with: + +- Main index.md linking to Platform docs +- docs/index.md as Platform documentation entry point +- Sections organized with toctree directives in MyST format +- Cross-references using intersphinx to Core and User documentation + +## Key Development Notes + +- Uses MyST parser for enhanced Markdown features with reStructuredText compatibility +- Custom sidebar template requires syncing via `scripts/sync_sidebar.py` after structural changes +- Intersphinx links to related Dash documentation (user and core docs) +- GitHub integration for edit links and source references +- Google Analytics tracking configured +- Uses pydata-sphinx-theme with custom CSS overrides + +## File Patterns + +- Documentation files: `docs/**/*.md` +- Configuration: `conf.py`, `requirements.txt` +- Templates: `_templates/*.html` +- Assets: `_static/**/*` +- Build output: `_build/html/` (excluded from git) \ No newline at end of file diff --git a/README.md b/README.md index 4ebd4cc49..e18d4a76f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,11 @@ Docs](https://readthedocs.org/). Feel free to [open an issue](https://github.com/dashpay/docs-platform/issues/new/choose) or submit PRs modifying the English source text in this repository. +Tutorial code blocks in `docs/tutorials/` are synced from the +[platform-tutorials](https://github.com/dashpay/platform-tutorials) repo. Run +`python3 scripts/tutorial-sync/sync_tutorial_code.py --check --source /path/to/platform-tutorials` +to check for drift, or without `--check` to update the docs in-place. + ## License [MIT](/LICENSE) © Dash Core Group, Inc. diff --git a/conf.py b/conf.py index c0648ca40..ed1695462 100644 --- a/conf.py +++ b/conf.py @@ -39,7 +39,7 @@ '.DS_Store', 'README.md', '.devcontainer', - 'local', + '.local', 'scripts', 'img/dev/gifs/README.md', 'docs/other', @@ -123,7 +123,7 @@ # "github_url": "https://github.com", # or your GitHub Enterprise site "github_user": "dashpay", "github_repo": "docs-platform", - "github_version": "2.0.0", + "github_version": "3.1.0", "doc_path": "", } diff --git a/docs/tutorials/create-and-fund-a-wallet.md b/docs/tutorials/create-and-fund-a-wallet.md index 849628381..9020b9fbe 100644 --- a/docs/tutorials/create-and-fund-a-wallet.md +++ b/docs/tutorials/create-and-fund-a-wallet.md @@ -21,9 +21,10 @@ const network = 'testnet'; try { const mnemonic = await wallet.generateMnemonic(); - const pathInfo = network === 'testnet' - ? await wallet.derivationPathBip44Testnet(0, 0, 0) - : await wallet.derivationPathBip44Mainnet(0, 0, 0); + const pathInfo = + network === 'testnet' + ? await wallet.derivationPathBip44Testnet(0, 0, 0) + : await wallet.derivationPathBip44Mainnet(0, 0, 0); // Derive the first BIP44 key to get a platform address const keyInfo = await wallet.deriveKeyFromSeedWithPath({ @@ -40,7 +41,10 @@ try { // ⚠️ Never log mnemonics in real applications console.log('Mnemonic:', mnemonic); console.log('Platform address:', address); - console.log('Fund address using:', `https://bridge.thepasta.org/?address=${address}`); + console.log( + 'Fund address using:', + `https://bridge.thepasta.org/?address=${address}`, + ); } catch (e) { console.error('Something went wrong:', e.message); } diff --git a/requirements.txt b/requirements.txt index 378311039..e713d54bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ sphinx==8.1.3 sphinx-copybutton==0.5.2 sphinx-hoverxref==1.4.2 sphinx_design==0.6.1 +pyyaml==6.0 sphinxcontrib-googleanalytics==0.4 diff --git a/scripts/tutorial-sync/sync_tutorial_code.py b/scripts/tutorial-sync/sync_tutorial_code.py new file mode 100644 index 000000000..0f2c6ccf2 --- /dev/null +++ b/scripts/tutorial-sync/sync_tutorial_code.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python3 +"""Sync inline code blocks in tutorial docs with platform-tutorials source files. + +Usage: + python scripts/tutorial-sync/sync_tutorial_code.py --source /path/to/platform-tutorials + python scripts/tutorial-sync/sync_tutorial_code.py --check --source /path/to/platform-tutorials + python scripts/tutorial-sync/sync_tutorial_code.py --diff --source /path/to/platform-tutorials + +Modes: + (default) Update docs in-place with source code + --check Exit non-zero if any code block differs (for CI) + --diff Like --check but also print unified diffs +""" + +import argparse +import difflib +import os +import re +import sys +from pathlib import Path + +try: + import yaml +except ImportError: + sys.exit("pyyaml is required: pip install pyyaml") + +SCRIPT_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = SCRIPT_DIR.parent.parent +DEFAULT_CONFIG = SCRIPT_DIR / "tutorial-code-map.yml" + + +# --------------------------------------------------------------------------- +# Source file reading +# --------------------------------------------------------------------------- + +def read_source(path: Path) -> str: + """Read a source file, stripping the '// See https://...' header if present.""" + text = path.read_text(encoding="utf-8") + lines = text.split("\n", 1) + if lines and re.match(r"^// See https?://", lines[0]): + text = lines[1] if len(lines) > 1 else "" + return text.rstrip("\n") + + +# --------------------------------------------------------------------------- +# Block finders — return (code_start, code_end) byte offsets of the code body +# --------------------------------------------------------------------------- + +def find_by_caption(content: str, caption: str, language: str = "javascript"): + """Find a {code-block} directive with a matching :caption: value. + + Returns (start, end) offsets of the code body between the directive + header and the closing ``` fence. + """ + # Match the opening directive + options, then the code body + # The directive looks like: + # ```{code-block} javascript + # :caption: connect.mjs + # [:name: ...] + # + # + # ``` + pattern = re.compile( + r"^```\{code-block\}\s+" + + re.escape(language) + + r"\s*\n" + + r"(?::[\w-]+:.*\n)*?" # options before caption + + r":caption:\s+" + + re.escape(caption) + + r"\s*\n" + + r"(?::[\w-]+:.*\n)*" # options after caption + + r"\n", # blank line before code body + re.MULTILINE, + ) + m = pattern.search(content) + if not m: + return None + + code_start = m.end() + + # Find closing ``` fence + close = re.compile(r"^```\s*$", re.MULTILINE) + cm = close.search(content, code_start) + if not cm: + return None + + # code_end is the position just before the newline preceding ``` + code_end = cm.start() + if code_end > code_start and content[code_end - 1] == "\n": + code_end -= 1 + + return (code_start, code_end) + + +def find_by_sync(content: str, sync_value: str, language: str = "javascript"): + """Find a fenced code block inside a tab-item with a matching :sync: value. + + When multiple tab-sets use the same sync values (e.g., JSON schemas and + JS code), the language parameter disambiguates. + """ + sync_pattern = re.compile( + r"^:sync:\s+" + re.escape(sync_value) + r"\s*$", re.MULTILINE + ) + + for m in sync_pattern.finditer(content): + search_start = m.end() + + # Find next fenced block with the target language + fence_open = re.compile( + r"^```" + re.escape(language) + r"\s*$", re.MULTILINE + ) + fm = fence_open.search(content, search_start) + if not fm: + continue + + # Ensure we haven't crossed into a different tab-item + between = content[search_start : fm.start()] + if ":::{tab-item}" in between or "::::" in between: + continue + + code_start = fm.end() + 1 # skip the newline after ```language + + fence_close = re.compile(r"^```\s*$", re.MULTILINE) + cm = fence_close.search(content, code_start) + if not cm: + continue + + code_end = cm.start() + if code_end > code_start and content[code_end - 1] == "\n": + code_end -= 1 + + return (code_start, code_end) + + return None + + +def find_by_tab(content: str, tab_title: str, language: str = "javascript"): + """Find a fenced code block inside a tab-item matched by its title.""" + tab_pattern = re.compile( + r"^:::\{tab-item\}\s+" + re.escape(tab_title) + r"\s*$", re.MULTILINE + ) + + for m in tab_pattern.finditer(content): + search_start = m.end() + + # Find next fenced block with the target language (skip options, prose) + fence_open = re.compile( + r"^```" + re.escape(language) + r"\s*$", re.MULTILINE + ) + fm = fence_open.search(content, search_start) + if not fm: + continue + + # Ensure we haven't crossed into a different tab-item or tab-set + between = content[search_start : fm.start()] + if ":::{tab-item}" in between or "::::" in between: + continue + + code_start = fm.end() + 1 + + fence_close = re.compile(r"^```\s*$", re.MULTILINE) + cm = fence_close.search(content, code_start) + if not cm: + continue + + code_end = cm.start() + if code_end > code_start and content[code_end - 1] == "\n": + code_end -= 1 + + return (code_start, code_end) + + return None + + +# --------------------------------------------------------------------------- +# Dispatcher +# --------------------------------------------------------------------------- + +def find_block(content: str, block_id: dict, language: str): + """Dispatch to the correct finder based on block_id keys.""" + if "caption" in block_id: + return find_by_caption(content, block_id["caption"], language) + elif "sync" in block_id: + return find_by_sync(content, block_id["sync"], language) + elif "tab" in block_id: + return find_by_tab(content, block_id["tab"], language) + else: + raise ValueError(f"Unknown block_id type: {block_id}") + + +# --------------------------------------------------------------------------- +# Main logic +# --------------------------------------------------------------------------- + +def process_mappings(config: dict, source_root: Path, mode: str): + """Process all mappings. Returns (matched, mismatched, errors) counts.""" + docs_root = PROJECT_ROOT / config["docs_root"] + matched = 0 + mismatched = 0 + errors = 0 + + for entry in config["mappings"]: + source_path = source_root / entry["source"] + doc_path = docs_root / entry["doc"] + block_id = entry["block_id"] + language = entry.get("language", "javascript") + label = f"{entry['source']} -> {entry['doc']}" + + # Validate files exist + if not source_path.is_file(): + print(f" ERROR {label}: source file not found: {source_path}") + errors += 1 + continue + if not doc_path.is_file(): + print(f" ERROR {label}: doc file not found: {doc_path}") + errors += 1 + continue + + # Read source code + source_code = read_source(source_path) + + # Read doc and find the block + doc_content = doc_path.read_text(encoding="utf-8") + result = find_block(doc_content, block_id, language) + + if result is None: + print(f" ERROR {label}: code block not found (block_id: {block_id})") + errors += 1 + continue + + code_start, code_end = result + existing_code = doc_content[code_start:code_end] + + # Compare + if existing_code.rstrip() == source_code.rstrip(): + print(f" MATCH {label}") + matched += 1 + else: + mismatched += 1 + if mode == "diff": + print(f" DIFF {label}") + diff = difflib.unified_diff( + existing_code.splitlines(keepends=True), + source_code.splitlines(keepends=True), + fromfile=f"docs: {entry['doc']}", + tofile=f"src: {entry['source']}", + ) + sys.stdout.writelines(" " + line for line in diff) + print() + elif mode == "check": + print(f" DRIFT {label}") + else: + # sync mode: replace in-place + updated = doc_content[:code_start] + source_code + doc_content[code_end:] + doc_path.write_text(updated, encoding="utf-8") + print(f" SYNCED {label}") + + return matched, mismatched, errors + + +def main(): + parser = argparse.ArgumentParser( + description="Sync tutorial code blocks with platform-tutorials source files." + ) + parser.add_argument( + "--source", + type=Path, + help="Path to platform-tutorials repo root", + ) + parser.add_argument( + "--config", + type=Path, + default=DEFAULT_CONFIG, + help="Path to mapping config YAML (default: scripts/tutorial-code-map.yml)", + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--check", + action="store_true", + help="Compare only, exit non-zero if any drift (for CI)", + ) + group.add_argument( + "--diff", + action="store_true", + help="Compare and show unified diffs", + ) + args = parser.parse_args() + + # Determine mode + if args.check: + mode = "check" + elif args.diff: + mode = "diff" + else: + mode = "sync" + + # Load config + with open(args.config, encoding="utf-8") as f: + config = yaml.safe_load(f) + + # Resolve source root: CLI > env var > None + source_root = args.source or os.environ.get("PLATFORM_TUTORIALS_PATH") + if source_root: + source_root = Path(source_root).resolve() + else: + sys.exit( + "Error: platform-tutorials path required.\n" + "Use --source /path/to/platform-tutorials or set PLATFORM_TUTORIALS_PATH" + ) + + if not source_root.is_dir(): + sys.exit(f"Error: source directory not found: {source_root}") + + print(f"Source: {source_root}") + print(f"Mode: {mode}") + print(f"Config: {args.config}") + print() + + matched, mismatched, errors = process_mappings(config, source_root, mode) + + print() + print(f"Summary: {matched} matched, {mismatched} drifted, {errors} errors") + print(f"Total: {matched + mismatched + errors} / {len(config['mappings'])} mappings") + + if mode in ("check", "diff") and (mismatched > 0 or errors > 0): + sys.exit(1) + elif errors > 0: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/scripts/tutorial-sync/tutorial-code-map.yml b/scripts/tutorial-sync/tutorial-code-map.yml new file mode 100644 index 000000000..68f241b23 --- /dev/null +++ b/scripts/tutorial-sync/tutorial-code-map.yml @@ -0,0 +1,173 @@ +# Maps platform-tutorials source files to inline code blocks in docs. +# +# source: path relative to platform-tutorials root +# doc: path relative to docs/tutorials/ +# block_id: how to locate the code block in the markdown: +# caption: match :caption: value in {code-block} directives +# sync: match :sync: value in tab-items (use with language filter) +# tab: match tab-item title text (use with language filter) +# language: fenced block language (default: javascript) + +docs_root: docs/tutorials + +mappings: + # -- Top-level tutorials -- + + - source: connect.mjs + doc: connecting-to-testnet.md + block_id: + caption: connect.mjs + + - source: create-wallet.mjs + doc: create-and-fund-a-wallet.md + block_id: + caption: create-wallet.mjs + + - source: setupDashClient.mjs + doc: setup-sdk-client.md + block_id: + caption: setupDashClient.mjs + + # send-funds.md excluded: source file uses old CommonJS SDK, + # docs already migrated to Evo SDK. Add when platform-tutorials updates. + + # -- Identities and Names -- + + - source: 1-Identities-and-Names/identity-register.mjs + doc: identities-and-names/register-an-identity.md + block_id: + caption: identity-register.mjs + + - source: 1-Identities-and-Names/identity-retrieve.mjs + doc: identities-and-names/retrieve-an-identity.md + block_id: + caption: identity-retrieve.mjs + + - source: 1-Identities-and-Names/identity-topup.mjs + doc: identities-and-names/topup-an-identity-balance.md + block_id: + caption: identity-topup.mjs + + - source: 1-Identities-and-Names/identity-withdraw-credits.mjs + doc: identities-and-names/withdraw-an-identity-balance.md + block_id: + caption: identity-withdraw-credits.mjs + + - source: 1-Identities-and-Names/identity-update-disable-key.mjs + doc: identities-and-names/update-an-identity.md + block_id: + caption: identity-update-disable-key.mjs + + - source: 1-Identities-and-Names/identity-update-add-key.mjs + doc: identities-and-names/update-an-identity.md + block_id: + caption: identity-update-add-key.mjs + + - source: 1-Identities-and-Names/identity-transfer-credits.mjs + doc: identities-and-names/transfer-credits-to-an-identity.md + block_id: + caption: identity-transfer-credits.mjs + + - source: 1-Identities-and-Names/name-register.mjs + doc: identities-and-names/register-a-name-for-an-identity.md + block_id: + caption: name-register.mjs + + - source: 1-Identities-and-Names/name-resolve-by-name.mjs + doc: identities-and-names/retrieve-a-name.md + block_id: + caption: name-resolve-by-name.mjs + + - source: 1-Identities-and-Names/name-get-identity-names.mjs + doc: identities-and-names/retrieve-a-name.md + block_id: + caption: name-get-identity-names.mjs + + - source: 1-Identities-and-Names/name-search-by-name.mjs + doc: identities-and-names/retrieve-a-name.md + block_id: + caption: name-search-by-name.mjs + + # -- Contracts and Documents -- + + # register-a-data-contract.md: 6 JS blocks in tab-set (second tab-set). + # First tab-set has JSON schemas with the same sync values -- language + # filter distinguishes them. + - source: 2-Contracts-and-Documents/contract-register-minimal.mjs + doc: contracts-and-documents/register-a-data-contract.md + block_id: + sync: minimal + language: javascript + + - source: 2-Contracts-and-Documents/contract-register-indexed.mjs + doc: contracts-and-documents/register-a-data-contract.md + block_id: + sync: indexed + language: javascript + + - source: 2-Contracts-and-Documents/contract-register-timestamps.mjs + doc: contracts-and-documents/register-a-data-contract.md + block_id: + sync: timestamp + language: javascript + + - source: 2-Contracts-and-Documents/contract-register-binary.mjs + doc: contracts-and-documents/register-a-data-contract.md + block_id: + sync: binary + language: javascript + + - source: 2-Contracts-and-Documents/contract-register-history.mjs + doc: contracts-and-documents/register-a-data-contract.md + block_id: + sync: history + language: javascript + + - source: 2-Contracts-and-Documents/contract-register-nft.mjs + doc: contracts-and-documents/register-a-data-contract.md + block_id: + sync: nft + language: javascript + + - source: 2-Contracts-and-Documents/contract-retrieve.mjs + doc: contracts-and-documents/retrieve-a-data-contract.md + block_id: + caption: contract-retrieve.mjs + + - source: 2-Contracts-and-Documents/contract-retrieve-history.mjs + doc: contracts-and-documents/retrieve-data-contract-history.md + block_id: + caption: contract-retrieve-history.mjs + + # update-a-data-contract.md: 2 JS blocks in tab-set, no :sync: or :caption: + - source: 2-Contracts-and-Documents/contract-update-minimal.mjs + doc: contracts-and-documents/update-a-data-contract.md + block_id: + tab: Minimal contract + language: javascript + + - source: 2-Contracts-and-Documents/contract-update-history.mjs + doc: contracts-and-documents/update-a-data-contract.md + block_id: + tab: Contract with history + language: javascript + + - source: 2-Contracts-and-Documents/document-submit.mjs + doc: contracts-and-documents/submit-documents.md + block_id: + caption: document-submit.mjs + + - source: 2-Contracts-and-Documents/document-retrieve.mjs + doc: contracts-and-documents/retrieve-documents.md + block_id: + caption: document-retrieve.mjs + + - source: 2-Contracts-and-Documents/document-update.mjs + doc: contracts-and-documents/update-documents.md + block_id: + caption: document-update.mjs + + - source: 2-Contracts-and-Documents/document-delete.mjs + doc: contracts-and-documents/delete-documents.md + block_id: + caption: document-delete.mjs